about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.envrc18
-rw-r--r--.gcroots/.skip-subtree1
-rw-r--r--.git-blame-ignore-revs26
-rw-r--r--.gitignore13
-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--LICENSE696
-rw-r--r--OWNERS6
-rw-r--r--README.md145
-rw-r--r--RULES1
-rw-r--r--buf.gen.yaml8
-rw-r--r--buf.yaml12
-rw-r--r--corp/LICENSE4
-rw-r--r--corp/OWNERS3
-rw-r--r--corp/ops/.envrc4
-rw-r--r--corp/ops/.gitignore4
-rw-r--r--corp/ops/default.nix46
-rw-r--r--corp/ops/modules/.skip-tree1
-rw-r--r--corp/ops/modules/yandex-cloud.nix79
-rw-r--r--corp/ops/yandex/creds.fish5
-rw-r--r--corp/ops/yandex/encrypted-state-secret.keybin0 -> 121 bytes
-rw-r--r--corp/ops/yandex/main.tf70
-rw-r--r--corp/ops/yandex/rih.tf69
-rw-r--r--corp/rih/.gitignore3
-rw-r--r--corp/rih/Cargo.lock1812
-rw-r--r--corp/rih/Cargo.toml39
-rw-r--r--corp/rih/README.md3
-rw-r--r--corp/rih/default.nix52
-rw-r--r--corp/rih/index.css62
-rw-r--r--corp/rih/index.html17
-rw-r--r--corp/rih/rih-logo.pngbin0 -> 420956 bytes
-rw-r--r--corp/rih/src/home.html178
-rw-r--r--corp/rih/src/main.rs395
-rw-r--r--corp/rih/src/privacy-policy.md100
-rw-r--r--corp/rih/static-markdown/Cargo.toml12
-rw-r--r--corp/rih/static-markdown/src/lib.rs27
-rw-r--r--corp/russian/README.md9
-rw-r--r--corp/russian/data-import/.gitignore2
-rw-r--r--corp/russian/data-import/Cargo.lock499
-rw-r--r--corp/russian/data-import/Cargo.toml18
-rw-r--r--corp/russian/data-import/default.nix55
-rw-r--r--corp/russian/data-import/src/db_setup.rs298
-rw-r--r--corp/russian/data-import/src/main.rs298
-rw-r--r--corp/russian/data-import/src/mappings.rs185
-rw-r--r--corp/russian/data-import/src/oc_parser.rs470
-rw-r--r--corp/russian/data-import/src/or_parser.rs105
-rw-r--r--corp/russian/predlozhnik/.gitignore3
-rw-r--r--corp/russian/predlozhnik/Cargo.lock460
-rw-r--r--corp/russian/predlozhnik/Cargo.toml12
-rw-r--r--corp/russian/predlozhnik/default.nix52
-rw-r--r--corp/russian/predlozhnik/index.css29
-rw-r--r--corp/russian/predlozhnik/index.html24
-rw-r--r--corp/russian/predlozhnik/src/main.rs345
-rw-r--r--corp/tvixbolt/.gitignore2
-rw-r--r--corp/tvixbolt/Cargo.lock1036
-rw-r--r--corp/tvixbolt/Cargo.toml26
-rw-r--r--corp/tvixbolt/default.nix74
-rw-r--r--corp/tvixbolt/index.css7
-rw-r--r--corp/tvixbolt/index.html15
-rw-r--r--corp/tvixbolt/src/main.rs342
-rw-r--r--corp/website/content-en.md94
-rw-r--r--corp/website/content-ru.md98
-rw-r--r--corp/website/default.nix59
-rw-r--r--default.nix123
-rw-r--r--docs/CODE_OF_CONDUCT.md29
-rw-r--r--docs/CONTRIBUTING.md120
-rw-r--r--docs/REVIEWS.md191
-rw-r--r--docs/designs/SPARSE_CHECKOUTS.md43
-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.nix56
-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.lock1733
-rw-r--r--fun/paroxysm/Cargo.toml22
-rw-r--r--fun/paroxysm/OWNERS1
-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/OWNERS1
-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/🕰️/OWNERS1
-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/OWNERS1
-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/OWNERS1
-rw-r--r--nix/binify/default.nix16
-rw-r--r--nix/bufCheck/default.nix22
-rw-r--r--nix/buildGo/.skip-subtree2
-rw-r--r--nix/buildGo/README.md117
-rw-r--r--nix/buildGo/default.nix121
-rw-r--r--nix/buildGo/example/default.nix40
-rw-r--r--nix/buildGo/example/lib.go9
-rw-r--r--nix/buildGo/example/main.go25
-rw-r--r--nix/buildGo/external/default.nix112
-rw-r--r--nix/buildGo/external/main.go201
-rw-r--r--nix/buildLisp/README.md254
-rw-r--r--nix/buildLisp/default.nix778
-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.nix58
-rw-r--r--nix/buildManPages/OWNERS1
-rw-r--r--nix/buildManPages/default.nix103
-rw-r--r--nix/buildkite/default.nix451
-rwxr-xr-xnix/buildkite/fetch-parent-targets.sh44
-rw-r--r--nix/dependency-analyzer/default.nix258
-rw-r--r--nix/dependency-analyzer/examples/ci-targets.nix12
-rw-r--r--nix/dependency-analyzer/examples/lisp.nix5
-rw-r--r--nix/dependency-analyzer/tests/default.nix36
-rw-r--r--nix/drvSeqL/default.nix47
-rw-r--r--nix/emptyDerivation/OWNERS1
-rw-r--r--nix/emptyDerivation/default.nix22
-rw-r--r--nix/emptyDerivation/emptyDerivation.nix36
-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.nix91
-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/OWNERS1
-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/nix-1p/README.md601
-rw-r--r--nix/nix-1p/default.nix16
-rw-r--r--nix/readTree/README.md95
-rw-r--r--nix/readTree/default.nix326
-rw-r--r--nix/readTree/tests/.skip-subtree1
-rw-r--r--nix/readTree/tests/default.nix139
-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-tree-traversal/skip-tree/a/default.nix1
-rw-r--r--nix/readTree/tests/test-tree-traversal/skip-tree/b/.skip-tree1
-rw-r--r--nix/readTree/tests/test-tree-traversal/skip-tree/b/default.nix1
-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/OWNERS1
-rw-r--r--nix/sparseTree/default.nix73
-rw-r--r--nix/stateMonad/default.nix76
-rw-r--r--nix/stateMonad/tests/default.nix110
-rw-r--r--nix/tag/default.nix166
-rw-r--r--nix/tag/tests.nix99
-rw-r--r--nix/tailscale/default.nix31
-rw-r--r--nix/utils/OWNERS1
-rw-r--r--nix/utils/default.nix160
-rw-r--r--nix/utils/tests/.skip-subtree1
-rw-r--r--nix/utils/tests/default.nix99
-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/buildkite/.gitignore2
-rw-r--r--ops/buildkite/README.md24
-rw-r--r--ops/buildkite/default.nix14
-rw-r--r--ops/buildkite/steps-depot.yml6
-rw-r--r--ops/buildkite/steps-tvix.yml4
-rw-r--r--ops/buildkite/steps-tvl-kit.yml4
-rw-r--r--ops/buildkite/tvl.tf48
-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.nix15
-rw-r--r--ops/glesys/dns-nixery-dev.tf44
-rw-r--r--ops/glesys/dns-tvix-dev.tf47
-rw-r--r--ops/glesys/dns-tvl-fyi.tf99
-rw-r--r--ops/glesys/dns-tvl-su.tf140
-rw-r--r--ops/glesys/main.tf87
-rw-r--r--ops/journaldriver/.gitignore3
-rw-r--r--ops/journaldriver/Cargo.lock640
-rw-r--r--ops/journaldriver/Cargo.toml21
-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.nix14
-rw-r--r--ops/keycloak/main.tf44
-rw-r--r--ops/keycloak/user_sources.tf44
-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.nix26
-rw-r--r--ops/machines/sanduny/default.nix130
-rw-r--r--ops/machines/whitby/OWNERS5
-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.nix55
-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/depot-inbox.nix148
-rw-r--r--ops/modules/depot-replica.nix45
-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.nix174
-rw-r--r--ops/modules/nixery.nix43
-rw-r--r--ops/modules/oauth2_proxy.nix60
-rw-r--r--ops/modules/open_eid.nix54
-rw-r--r--ops/modules/owothia.nix68
-rw-r--r--ops/modules/panettone.nix119
-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.nix95
-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.nix28
-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.nix34
-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/inbox.tvl.su.nix31
-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.nix49
-rw-r--r--ops/modules/www/todo.tvl.fyi.nix25
-rw-r--r--ops/modules/www/tvix.dev.nix39
-rw-r--r--ops/modules/www/tvixbolt.tvl.su.nix19
-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.yaml129
-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.agebin0 -> 1186 bytes
-rw-r--r--ops/secrets/buildkite-agent-token.agebin0 -> 743 bytes
-rw-r--r--ops/secrets/buildkite-graphql-token.age16
-rw-r--r--ops/secrets/buildkite-ssh-private-key.agebin0 -> 1194 bytes
-rw-r--r--ops/secrets/clbot-ssh.agebin0 -> 1162 bytes
-rw-r--r--ops/secrets/clbot.age15
-rw-r--r--ops/secrets/default.nix3
-rw-r--r--ops/secrets/depot-inbox-imap.age15
-rw-r--r--ops/secrets/depot-replica-key.agebin0 -> 1208 bytes
-rw-r--r--ops/secrets/gerrit-queue.agebin0 -> 853 bytes
-rw-r--r--ops/secrets/gerrit-secrets.agebin0 -> 913 bytes
-rw-r--r--ops/secrets/grafana.age16
-rw-r--r--ops/secrets/irccat.agebin0 -> 825 bytes
-rw-r--r--ops/secrets/journaldriver.agebin0 -> 3202 bytes
-rw-r--r--ops/secrets/keycloak-db.age15
-rw-r--r--ops/secrets/mkSecrets.nix27
-rw-r--r--ops/secrets/nix-cache-priv.age15
-rw-r--r--ops/secrets/nix-cache-pub.age16
-rw-r--r--ops/secrets/oauth2_proxy.agebin0 -> 917 bytes
-rw-r--r--ops/secrets/owothia.age16
-rw-r--r--ops/secrets/panettone.age15
-rw-r--r--ops/secrets/secrets.nix55
-rw-r--r--ops/secrets/smtprelay.age16
-rw-r--r--ops/secrets/tf-buildkite.agebin0 -> 943 bytes
-rw-r--r--ops/secrets/tf-glesys.agebin0 -> 959 bytes
-rw-r--r--ops/secrets/tf-keycloak.agebin0 -> 962 bytes
-rw-r--r--ops/secrets/tvl-alerts-bot-telegram-token.age15
-rw-r--r--ops/terraform/README.md5
-rw-r--r--ops/terraform/deploy-nixos/README.md44
-rw-r--r--ops/terraform/deploy-nixos/main.tf109
-rwxr-xr-xops/terraform/deploy-nixos/nixos-copy.sh27
-rwxr-xr-xops/terraform/deploy-nixos/nixos-eval.sh15
-rw-r--r--ops/users/default.nix217
-rw-r--r--ops/yandex-cloud-rs/.gitignore5
-rw-r--r--ops/yandex-cloud-rs/Cargo.lock1394
-rw-r--r--ops/yandex-cloud-rs/Cargo.toml21
-rw-r--r--ops/yandex-cloud-rs/README.md49
-rw-r--r--ops/yandex-cloud-rs/build.rs43
-rw-r--r--ops/yandex-cloud-rs/default.nix22
-rw-r--r--ops/yandex-cloud-rs/examples/log-write.rs37
-rw-r--r--ops/yandex-cloud-rs/src/lib.rs95
-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/OWNERS1
-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/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.css889
-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.c564
-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/OWNERS1
-rw-r--r--third_party/clj2nix/default.nix9
-rw-r--r--third_party/default.nix56
-rw-r--r--third_party/elmPackages_0_18/default.nix21
-rw-r--r--third_party/emacs/rcirc/default.nix7
-rw-r--r--third_party/emacs/rcirc/rcirc.el3133
-rw-r--r--third_party/exwm/.elpaignore (renamed from .elpaignore)0
-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-background.el (renamed from exwm-background.el)0
-rw-r--r--third_party/exwm/exwm-cm.el (renamed from exwm-cm.el)0
-rw-r--r--third_party/exwm/exwm-config.el (renamed from exwm-config.el)0
-rw-r--r--third_party/exwm/exwm-core.el (renamed from exwm-core.el)0
-rw-r--r--third_party/exwm/exwm-floating.el (renamed from exwm-floating.el)0
-rw-r--r--third_party/exwm/exwm-input.el (renamed from exwm-input.el)0
-rw-r--r--third_party/exwm/exwm-layout.el (renamed from exwm-layout.el)0
-rw-r--r--third_party/exwm/exwm-manage.el (renamed from exwm-manage.el)0
-rw-r--r--third_party/exwm/exwm-randr.el (renamed from exwm-randr.el)0
-rw-r--r--third_party/exwm/exwm-systemtray.el (renamed from exwm-systemtray.el)0
-rw-r--r--third_party/exwm/exwm-workspace.el (renamed from exwm-workspace.el)0
-rw-r--r--third_party/exwm/exwm-xim.el (renamed from exwm-xim.el)0
-rw-r--r--third_party/exwm/exwm.el (renamed from exwm.el)0
-rw-r--r--third_party/exwm/xinitrc (renamed from xinitrc)0
-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-Syntax-highlight-nix.patch37
-rw-r--r--third_party/gerrit/0002-Syntax-highlight-rules.pl.patch37
-rw-r--r--third_party/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch215
-rw-r--r--third_party/gerrit/default.nix158
-rw-r--r--third_party/gerrit/detzip.go97
-rw-r--r--third_party/gerrit_plugins/builder.nix39
-rw-r--r--third_party/gerrit_plugins/code-owners/default.nix17
-rw-r--r--third_party/gerrit_plugins/code-owners/using-usernames.patch481
-rw-r--r--third_party/gerrit_plugins/oauth/default.nix19
-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/OWNERS1
-rw-r--r--third_party/irccat/default.nix16
-rw-r--r--third_party/josh/default.nix37
-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/OWNERS2
-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.nix53
-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.nix33
-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/OWNERS1
-rw-r--r--third_party/lisp/mime4cl/README.md27
-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.lisp663
-rw-r--r--third_party/lisp/mime4cl/ex-sclf.lisp368
-rw-r--r--third_party/lisp/mime4cl/mime.lisp1049
-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.lisp103
-rw-r--r--third_party/lisp/mime4cl/streams.lisp274
-rw-r--r--third_party/lisp/mime4cl/test/address.lisp123
-rw-r--r--third_party/lisp/mime4cl/test/endec.lisp184
-rw-r--r--third_party/lisp/mime4cl/test/mime.lisp41
-rw-r--r--third_party/lisp/mime4cl/test/package.lisp27
-rw-r--r--third_party/lisp/mime4cl/test/rt.lisp258
-rw-r--r--third_party/lisp/mime4cl/test/samples/sample1.msg86
-rw-r--r--third_party/lisp/mime4cl/test/temp-file.lisp72
-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/OWNERS1
-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/qbase64/coreutils-base64.patch13
-rw-r--r--third_party/lisp/qbase64/default.nix57
-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/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/napalm/default.nix7
-rw-r--r--third_party/nixpkgs/default.nix81
-rw-r--r--third_party/nsfv/default.nix23
-rw-r--r--third_party/overlays/dhall/OWNERS1
-rw-r--r--third_party/overlays/dhall/default.nix30
-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.nix48
-rw-r--r--third_party/overlays/haskell/extra-pkgs/brick-0.73.nix70
-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/buf-tests-dont-use-file-transport.patch64
-rw-r--r--third_party/overlays/patches/notmuch-dottime.patch81
-rw-r--r--third_party/overlays/tvl.nix128
-rw-r--r--third_party/prometheus-fail2ban-exporter/default.nix18
-rw-r--r--third_party/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch30
-rw-r--r--third_party/public-inbox/default.nix9
-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/OWNERS3
-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.json122
-rw-r--r--third_party/terraform-provider-glesys/default.nix19
-rw-r--r--tools/checks/default.nix38
-rw-r--r--tools/cheddar/.gitignore1
-rw-r--r--tools/cheddar/.skip-subtree0
-rw-r--r--tools/cheddar/Cargo.lock1587
-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.nix24
-rw-r--r--tools/cheddar/src/bin/cheddar.rs134
-rw-r--r--tools/cheddar/src/lib.rs347
-rw-r--r--tools/cheddar/src/tests.rs110
-rw-r--r--tools/crfo-approve.nix52
-rw-r--r--tools/depot-deps.nix33
-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/OWNERS1
-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/OWNERS1
-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/OWNERS1
-rw-r--r--tools/emacs-pkgs/tvl/default.nix8
-rw-r--r--tools/emacs-pkgs/tvl/tvl.el244
-rw-r--r--tools/eprintf.nix15
-rw-r--r--tools/fetch-depot-inbox.nix49
-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.scm366
-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.go527
-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.go283
-rw-r--r--tools/nixery/config/config.go73
-rw-r--r--tools/nixery/config/pkgsource.go148
-rw-r--r--tools/nixery/default.nix131
-rw-r--r--tools/nixery/go.mod14
-rw-r--r--tools/nixery/go.sum708
-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.nix198
-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/nixery/web/index.html166
-rw-r--r--tools/nixery/web/nixery-logo.pngbin0 -> 194098 bytes
-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/OWNERS2
-rw-r--r--tools/rust-crates-advisory/default.nix121
-rw-r--r--tools/rust-crates-advisory/format-audit-result.jq75
-rw-r--r--tvix/.gitignore4
-rw-r--r--tvix/.vscode/extensions.json8
-rw-r--r--tvix/Cargo.lock3104
-rw-r--r--tvix/Cargo.nix10624
-rw-r--r--tvix/Cargo.toml35
-rw-r--r--tvix/LICENSE674
-rw-r--r--tvix/OWNERS7
-rw-r--r--tvix/README.md104
-rw-r--r--tvix/cli/Cargo.toml23
-rw-r--r--tvix/cli/default.nix36
-rw-r--r--tvix/cli/src/.skip-subtree1
-rw-r--r--tvix/cli/src/derivation.nix36
-rw-r--r--tvix/cli/src/derivation.rs697
-rw-r--r--tvix/cli/src/errors.rs28
-rw-r--r--tvix/cli/src/fetchurl.nix53
-rw-r--r--tvix/cli/src/known_paths.rs186
-rw-r--r--tvix/cli/src/main.rs284
-rw-r--r--tvix/cli/src/nix_compat.rs122
-rw-r--r--tvix/cli/src/refscan.rs115
-rw-r--r--tvix/cli/src/tvix_io.rs79
-rw-r--r--tvix/crate-hashes.json5
-rw-r--r--tvix/default.nix90
-rw-r--r--tvix/docs/.gitignore2
-rw-r--r--tvix/docs/Makefile12
-rw-r--r--tvix/docs/component-flow.puml60
-rw-r--r--tvix/docs/components.md160
-rw-r--r--tvix/docs/default.nix47
-rw-r--r--tvix/docs/lang-version.md60
-rw-r--r--tvix/docs/language-spec.md78
-rw-r--r--tvix/docs/value-pointer-equality.md202
-rw-r--r--tvix/eval/.skip-subtree2
-rw-r--r--tvix/eval/Cargo.toml60
-rw-r--r--tvix/eval/README.md58
-rw-r--r--tvix/eval/benches/eval.rs39
-rw-r--r--tvix/eval/build.rs9
-rw-r--r--tvix/eval/builtin-macros/.gitignore1
-rw-r--r--tvix/eval/builtin-macros/Cargo.toml16
-rw-r--r--tvix/eval/builtin-macros/src/lib.rs334
-rw-r--r--tvix/eval/builtin-macros/tests/tests.rs45
-rw-r--r--tvix/eval/default.nix9
-rw-r--r--tvix/eval/docs/abandoned/thread-local-vm.md233
-rw-r--r--tvix/eval/docs/build-references.md175
-rw-r--r--tvix/eval/docs/builtins.md138
-rw-r--r--tvix/eval/docs/known-optimisation-potential.md130
-rw-r--r--tvix/eval/docs/language-issues.md46
-rw-r--r--tvix/eval/docs/opcodes-attrsets.md122
-rw-r--r--tvix/eval/docs/recursive-attrs.md68
-rw-r--r--tvix/eval/docs/vm-loop.md315
-rw-r--r--tvix/eval/proptest-regressions/value/mod.txt10
-rw-r--r--tvix/eval/src/builtins/impure.rs81
-rw-r--r--tvix/eval/src/builtins/mod.rs1037
-rw-r--r--tvix/eval/src/builtins/to_xml.rs139
-rw-r--r--tvix/eval/src/builtins/versions.rs155
-rw-r--r--tvix/eval/src/chunk.rs279
-rw-r--r--tvix/eval/src/compiler/bindings.rs826
-rw-r--r--tvix/eval/src/compiler/import.rs116
-rw-r--r--tvix/eval/src/compiler/mod.rs1497
-rw-r--r--tvix/eval/src/compiler/optimiser.rs125
-rw-r--r--tvix/eval/src/compiler/scope.rs378
-rw-r--r--tvix/eval/src/errors.rs1054
-rw-r--r--tvix/eval/src/io.rs138
-rw-r--r--tvix/eval/src/lib.rs346
-rw-r--r--tvix/eval/src/nix_search_path.rs243
-rw-r--r--tvix/eval/src/observer.rs287
-rw-r--r--tvix/eval/src/opcode.rs186
-rw-r--r--tvix/eval/src/pretty_ast.rs468
-rw-r--r--tvix/eval/src/properties.rs164
-rw-r--r--tvix/eval/src/source.rs63
-rw-r--r--tvix/eval/src/spans.rs121
-rw-r--r--tvix/eval/src/systems.rs351
-rw-r--r--tvix/eval/src/test_utils.rs8
-rw-r--r--tvix/eval/src/tests/mod.rs181
-rw-r--r--tvix/eval/src/tests/nix_tests/README.md8
-rw-r--r--tvix/eval/src/tests/nix_tests/binary-databin0 -> 1024 bytes
-rw-r--r--tvix/eval/src/tests/nix_tests/data1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir1/a.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir2/a.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir2/b.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir3/a.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir3/b.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir3/c.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir4/a.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/dir4/c.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-abort.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-assert.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-1.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-3.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-blackhole.nix-disabled5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-deepseq.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-float.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-int.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-hashfile-missing.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-missing-arg.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-path-slash.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-remove.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-seq.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-substring.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-fail-to-path.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-any-all.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-any-all.nix11
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.nix59
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrnames.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrnames.nix11
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs2.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs2.nix10
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs3.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs3.nix22
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs4.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs4.nix7
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs5.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-attrs5.nix21
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.nix2
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.nix2
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.nix8
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-builtins.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-builtins.nix12
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-catattrs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-catattrs.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-closure.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-closure.nix13
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-comments.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-comments.nix59
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-concat.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-concat.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-concatmap.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-concatmap.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.nix8
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-deepseq.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-deepseq.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.nix24
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.nix17
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.nix17
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-elem.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-elem.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-empty-args.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-empty-args.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-eq.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-eq.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-filter.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-filter.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-flatten.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-flatten.nix8
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-float.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-float.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.nix208
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-fromjson.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-fromjson.nix35
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-hash.exp0
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-if.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-if.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-ind-string.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-ind-string.nix128
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-let.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-let.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-list.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-list.nix7
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.nix11
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-logic.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-logic.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-map.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-map.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-nested-with.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-nested-with.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-new-let.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-new-let.nix14
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-partition.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-partition.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-pathexists.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-pathexists.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-patterns.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-patterns.nix16
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-readfile.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-readfile.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regex-match.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regex-match.nix29
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regex-split.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regex-split.nix48
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.nix2
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-remove.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-remove.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.nix11
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-1.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-1.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-2.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-2.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-3.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-3.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-4.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-4.nix10
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-6.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-6.nix7
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-7.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-scope-7.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-seq.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-seq.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-sort.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-sort.nix20
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-splitversion.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-splitversion.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-string.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-string.nix12
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.nix20
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-substring.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-substring.nix21
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-tojson.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-tojson.nix13
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-tryeval.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-tryeval.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-types.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-types.nix37
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-versions.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-versions.nix43
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-with.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-with.nix19
-rw-r--r--tvix/eval/src/tests/nix_tests/imported.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/imported2.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/lib.nix61
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-bad-antiquote-2.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-nonexist-path.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-scope-5.nix10
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-undeclared-arg.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.flags1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.nix15
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-closure.exp.xml343
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.nix24
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.nix29
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.nix10
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.nix9
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.exp.xml15
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.nix80
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.nix6
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.nix5
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.nix11
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.nix9
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-path.nix7
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.nix.disabled1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.flags1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.nix10
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.exp.xml52
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.nix21
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.exp1
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.nix9
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/readDir/bar0
-rw-r--r--tvix/eval/src/tests/nix_tests/notyetpassing/readDir/foo/git-hates-directories0
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-1.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-2.nix13
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-3.nix13
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-4.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-7.nix9
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-dup-formals.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-eof-in-string.nix3
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs1.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs2.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-patterns-1.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-regression-20060610.nix11
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-uft8.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-undef-var-2.nix7
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-fail-undef-var.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-1.nix1
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-crlf.nix17
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-5.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-6.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-1.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-2.nix4
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-3.nix7
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-regression-20041027.nix11
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-regression-751.nix2
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-subversion.nix43
-rw-r--r--tvix/eval/src/tests/nix_tests/parse-okay-url.nix8
-rw-r--r--tvix/eval/src/tests/one_offs.rs36
-rw-r--r--tvix/eval/src/tests/tvix_tests/README.md19
-rw-r--r--tvix/eval/src/tests/tvix_tests/directory/default.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-builtins-thunk-error.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-notcallable.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-strong.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-closed-formals.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-deep-forced-thunk-error.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-deepseq.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-foldlStrict-strict-op-application.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-force-before-value-pointer-equality.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-getEnv-coercion.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-infinite-recursion.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-outer-value-never-pointer-equal.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-parsedrvname-coerce.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-remove.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-fail-seq.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.nix9
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.nix2
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.nix10
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.nix15
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.nix15
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.nix10
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.nix10
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.nix12
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.nix10
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.nix46
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.nix11
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.nix9
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.nix15
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.nix19
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.nix10
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.nix18
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix31
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.nix23
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.nix11
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.nix9
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.nix22
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.nix34
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-ceil.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-ceil.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.nix14
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.nix14
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.nix17
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.nix3
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.nix3
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.nix18
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-dirof.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-dirof.nix10
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-elem.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-elem.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fib.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fib.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fix.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fix.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.nix2
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-floor.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-floor.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix3
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix24
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.nix80
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-genlist.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-genlist.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.nix30
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-import.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-import.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.nix3
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.nix3
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.nix3
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.nix2
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.nix8
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.nix15
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.nix9
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.nix12
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.nix20
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.nix9
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.nix15
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.nix9
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.nix9
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.nix12
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.nix2
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.nix14
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.nix26
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.nix17
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.nix21
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.nix2
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.nix12
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.nix2
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-readDir.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-readDir.nix.disabled1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-readfile.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-readfile.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.nix29
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-remove.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-remove.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-seq.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-seq.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.nix4
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.nix10
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.nix7
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.nix15
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.nix6
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.nix46
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.nix5
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.nix12
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.nix13
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-bool-false.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-bool-true.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-dollar-escape.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-empty-attrs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-empty-list.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-flat-attrs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-float.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-heterogeneous-list.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-homogeneous-float-list.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-homogeneous-int-list.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-homogeneous-string-list.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-int.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-kv-attrs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-nested-attrs.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-null.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-signed-float.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-signed-int.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/identity-string.nix1
-rw-r--r--tvix/eval/src/tests/tvix_tests/lib.nix61
-rw-r--r--tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.nix35
-rw-r--r--tvix/eval/src/tests/tvix_tests/readDir/bar0
-rw-r--r--tvix/eval/src/tests/tvix_tests/readDir/foo/.keep0
-rw-r--r--tvix/eval/src/upvalues.rs86
-rw-r--r--tvix/eval/src/value/arbitrary.rs106
-rw-r--r--tvix/eval/src/value/attrs.rs609
-rw-r--r--tvix/eval/src/value/attrs/tests.rs108
-rw-r--r--tvix/eval/src/value/builtin.rs136
-rw-r--r--tvix/eval/src/value/function.rs108
-rw-r--r--tvix/eval/src/value/json.rs84
-rw-r--r--tvix/eval/src/value/list.rs152
-rw-r--r--tvix/eval/src/value/mod.rs842
-rw-r--r--tvix/eval/src/value/path.rs14
-rw-r--r--tvix/eval/src/value/string.rs262
-rw-r--r--tvix/eval/src/value/thunk.rs365
-rw-r--r--tvix/eval/src/vm/generators.rs786
-rw-r--r--tvix/eval/src/vm/macros.rs70
-rw-r--r--tvix/eval/src/vm/mod.rs1235
-rw-r--r--tvix/eval/src/warnings.rs142
-rw-r--r--tvix/eval/tests/nix_oracle.rs157
-rw-r--r--tvix/nix-compat/Cargo.toml25
-rw-r--r--tvix/nix-compat/default.nix5
-rw-r--r--tvix/nix-compat/src/derivation/errors.rs59
-rw-r--r--tvix/nix-compat/src/derivation/mod.rs240
-rw-r--r--tvix/nix-compat/src/derivation/output.rs36
-rw-r--r--tvix/nix-compat/src/derivation/string_escape.rs17
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv.json23
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv.json19
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv.json23
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv.json19
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv.json16
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv.json23
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv.json23
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv1
-rw-r--r--tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv.json23
-rw-r--r--tvix/nix-compat/src/derivation/tests/mod.rs315
-rw-r--r--tvix/nix-compat/src/derivation/validate.rs129
-rw-r--r--tvix/nix-compat/src/derivation/write.rs190
-rw-r--r--tvix/nix-compat/src/lib.rs5
-rw-r--r--tvix/nix-compat/src/nar/mod.rs1
-rw-r--r--tvix/nix-compat/src/nar/writer/mod.rs219
-rw-r--r--tvix/nix-compat/src/nar/writer/wire.rs46
-rw-r--r--tvix/nix-compat/src/nixbase32.rs167
-rw-r--r--tvix/nix-compat/src/nixhash/algos.rs39
-rw-r--r--tvix/nix-compat/src/nixhash/mod.rs454
-rw-r--r--tvix/nix-compat/src/nixhash/with_mode.rs136
-rw-r--r--tvix/nix-compat/src/store_path/mod.rs294
-rw-r--r--tvix/nix-compat/src/store_path/utils.rs289
-rw-r--r--tvix/nix_cli/Cargo.toml18
-rw-r--r--tvix/nix_cli/default.nix5
-rw-r--r--tvix/nix_cli/src/bin/nix-store.rs105
-rw-r--r--tvix/nix_cli/src/main.rs3
-rw-r--r--tvix/proto/LICENSE21
-rw-r--r--tvix/proto/default.nix9
-rw-r--r--tvix/proto/evaluator.proto144
-rw-r--r--tvix/serde/.skip-subtree1
-rw-r--r--tvix/serde/Cargo.toml8
-rw-r--r--tvix/serde/default.nix5
-rw-r--r--tvix/serde/examples/cfg-demo.rs35
-rw-r--r--tvix/serde/examples/foods.nix22
-rw-r--r--tvix/serde/src/de.rs447
-rw-r--r--tvix/serde/src/de_tests.rs200
-rw-r--r--tvix/serde/src/error.rs102
-rw-r--r--tvix/serde/src/lib.rs11
-rw-r--r--tvix/store/Cargo.toml48
-rw-r--r--tvix/store/README.md59
-rw-r--r--tvix/store/build.rs32
-rw-r--r--tvix/store/default.nix5
-rw-r--r--tvix/store/docs/api.md280
-rw-r--r--tvix/store/docs/castore.md50
-rw-r--r--tvix/store/docs/why-not-git-trees.md57
-rw-r--r--tvix/store/protos/LICENSE21
-rw-r--r--tvix/store/protos/castore.go163
-rw-r--r--tvix/store/protos/castore.pb.go450
-rw-r--r--tvix/store/protos/castore.proto62
-rw-r--r--tvix/store/protos/castore_test.go271
-rw-r--r--tvix/store/protos/default.nix12
-rw-r--r--tvix/store/protos/go.mod19
-rw-r--r--tvix/store/protos/go.sum96
-rw-r--r--tvix/store/protos/pathinfo.pb.go498
-rw-r--r--tvix/store/protos/pathinfo.proto66
-rw-r--r--tvix/store/protos/rpc_blobstore.pb.go542
-rw-r--r--tvix/store/protos/rpc_blobstore.proto80
-rw-r--r--tvix/store/protos/rpc_blobstore_grpc.pb.go278
-rw-r--r--tvix/store/protos/rpc_directory.pb.go271
-rw-r--r--tvix/store/protos/rpc_directory.proto48
-rw-r--r--tvix/store/protos/rpc_directory_grpc.pb.go238
-rw-r--r--tvix/store/protos/rpc_pathinfo.pb.go284
-rw-r--r--tvix/store/protos/rpc_pathinfo.proto73
-rw-r--r--tvix/store/protos/rpc_pathinfo_grpc.pb.go246
-rw-r--r--tvix/store/src/bin/tvix-store.rs206
-rw-r--r--tvix/store/src/blobservice/grpc.rs217
-rw-r--r--tvix/store/src/blobservice/memory.rs76
-rw-r--r--tvix/store/src/blobservice/mod.rs41
-rw-r--r--tvix/store/src/blobservice/sled.rs94
-rw-r--r--tvix/store/src/digests.rs49
-rw-r--r--tvix/store/src/directoryservice/grpc.rs532
-rw-r--r--tvix/store/src/directoryservice/memory.rs84
-rw-r--r--tvix/store/src/directoryservice/mod.rs54
-rw-r--r--tvix/store/src/directoryservice/sled.rs107
-rw-r--r--tvix/store/src/directoryservice/traverse.rs222
-rw-r--r--tvix/store/src/directoryservice/utils.rs140
-rw-r--r--tvix/store/src/errors.rs45
-rw-r--r--tvix/store/src/import.rs220
-rw-r--r--tvix/store/src/lib.rs17
-rw-r--r--tvix/store/src/nar/grpc_nar_calculation_service.rs69
-rw-r--r--tvix/store/src/nar/mod.rs35
-rw-r--r--tvix/store/src/nar/non_caching_calculation_service.rs37
-rw-r--r--tvix/store/src/nar/renderer.rs136
-rw-r--r--tvix/store/src/pathinfoservice/grpc.rs81
-rw-r--r--tvix/store/src/pathinfoservice/memory.rs41
-rw-r--r--tvix/store/src/pathinfoservice/mod.rs20
-rw-r--r--tvix/store/src/pathinfoservice/sled.rs76
-rw-r--r--tvix/store/src/proto/grpc_blobservice_wrapper.rs130
-rw-r--r--tvix/store/src/proto/grpc_directoryservice_wrapper.rs177
-rw-r--r--tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs93
-rw-r--r--tvix/store/src/proto/mod.rs370
-rw-r--r--tvix/store/src/proto/sync_read_into_async_read.rs158
-rw-r--r--tvix/store/src/proto/tests/directory.rs289
-rw-r--r--tvix/store/src/proto/tests/directory_nodes_iterator.rs80
-rw-r--r--tvix/store/src/proto/tests/grpc_blobservice.rs102
-rw-r--r--tvix/store/src/proto/tests/grpc_directoryservice.rs241
-rw-r--r--tvix/store/src/proto/tests/grpc_pathinfoservice.rs67
-rw-r--r--tvix/store/src/proto/tests/mod.rs6
-rw-r--r--tvix/store/src/proto/tests/pathinfo.rs207
-rw-r--r--tvix/store/src/store_io.rs358
-rw-r--r--tvix/store/src/tests/fixtures.rs175
-rw-r--r--tvix/store/src/tests/import.rs113
-rw-r--r--tvix/store/src/tests/mod.rs4
-rw-r--r--tvix/store/src/tests/nar_renderer.rs189
-rw-r--r--tvix/store/src/tests/utils.rs17
-rw-r--r--tvix/verify-lang-tests/default.nix197
-rw-r--r--tvix/website/default.nix32
-rw-r--r--tvix/website/landing-en.md36
-rw-r--r--tvix/website/tvix-logo.webpbin0 -> 82366 bytes
-rw-r--r--users/Profpatsch/.envrc1
-rw-r--r--users/Profpatsch/.gitignore1
-rw-r--r--users/Profpatsch/.hlint.yaml355
-rw-r--r--users/Profpatsch/.vscode/settings.json14
-rw-r--r--users/Profpatsch/OWNERS4
-rw-r--r--users/Profpatsch/README.md10
-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/alacritty.nix31
-rw-r--r--users/Profpatsch/aliases.nix75
-rw-r--r--users/Profpatsch/arglib/ArglibNetencode.hs25
-rw-r--r--users/Profpatsch/arglib/arglib-netencode.cabal17
-rw-r--r--users/Profpatsch/arglib/netencode.nix79
-rw-r--r--users/Profpatsch/atomically-write.nix29
-rw-r--r--users/Profpatsch/blog/README.md7
-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/cas-serve/CasServe.hs239
-rw-r--r--users/Profpatsch/cas-serve/cas-serve.cabal23
-rw-r--r--users/Profpatsch/cas-serve/default.nix17
-rw-r--r--users/Profpatsch/cas-serve/schema.sql38
-rw-r--r--users/Profpatsch/cas-serve/wordlist.json1
-rw-r--r--users/Profpatsch/cas-serve/wordlist.sqlitebin0 -> 36864 bytes
-rw-r--r--users/Profpatsch/cdb.nix93
-rw-r--r--users/Profpatsch/dhall/lib.dhall84
-rw-r--r--users/Profpatsch/emacs-tree-sitter-move/README.md5
-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/ExecHelpers.hs48
-rw-r--r--users/Profpatsch/execline/default.nix48
-rw-r--r--users/Profpatsch/execline/exec-helpers.cabal14
-rw-r--r--users/Profpatsch/execline/exec-helpers/Cargo.lock7
-rw-r--r--users/Profpatsch/execline/exec-helpers/Cargo.toml8
-rw-r--r--users/Profpatsch/execline/exec-helpers/default.nix6
-rw-r--r--users/Profpatsch/execline/exec-helpers/exec_helpers.rs149
-rw-r--r--users/Profpatsch/fafo.jpgbin0 -> 26139 bytes
-rw-r--r--users/Profpatsch/git-db/default.nix10
-rw-r--r--users/Profpatsch/git-db/git-db.rs90
-rw-r--r--users/Profpatsch/haskell-module-deps/README.md5
-rw-r--r--users/Profpatsch/haskell-module-deps/default.nix55
-rw-r--r--users/Profpatsch/haskell-module-deps/example-output-dhall-haskell.pngbin0 -> 415873 bytes
-rw-r--r--users/Profpatsch/ical-smolify/IcalSmolify.hs124
-rw-r--r--users/Profpatsch/ical-smolify/README.md5
-rw-r--r--users/Profpatsch/ical-smolify/default.nix16
-rw-r--r--users/Profpatsch/ical-smolify/ical-smolify.cabal18
-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/ini/default.nix6
-rw-r--r--users/Profpatsch/ini/ini.dhall36
-rw-r--r--users/Profpatsch/lens.nix137
-rw-r--r--users/Profpatsch/lib.nix108
-rw-r--r--users/Profpatsch/lorri-wait-for-eval/LorriWaitForEval.hs173
-rw-r--r--users/Profpatsch/lorri-wait-for-eval/README.md7
-rw-r--r--users/Profpatsch/lorri-wait-for-eval/default.nix20
-rw-r--r--users/Profpatsch/mailbox-org/AesonQQ.hs23
-rw-r--r--users/Profpatsch/mailbox-org/MailboxOrg.hs527
-rw-r--r--users/Profpatsch/mailbox-org/README.md7
-rw-r--r--users/Profpatsch/mailbox-org/default.nix35
-rw-r--r--users/Profpatsch/mailbox-org/mailbox-org.cabal38
-rw-r--r--users/Profpatsch/my-prelude/Aeson.hs188
-rw-r--r--users/Profpatsch/my-prelude/Data/Error/Tree.hs113
-rw-r--r--users/Profpatsch/my-prelude/Label.hs120
-rw-r--r--users/Profpatsch/my-prelude/MyPrelude.hs587
-rw-r--r--users/Profpatsch/my-prelude/Pretty.hs91
-rw-r--r--users/Profpatsch/my-prelude/README.md42
-rw-r--r--users/Profpatsch/my-prelude/RunCommand.hs162
-rw-r--r--users/Profpatsch/my-prelude/Test.hs115
-rw-r--r--users/Profpatsch/my-prelude/default.nix41
-rw-r--r--users/Profpatsch/my-prelude/my-prelude.cabal44
-rw-r--r--users/Profpatsch/netencode/Netencode.hs445
-rw-r--r--users/Profpatsch/netencode/Netencode/Parse.hs112
-rw-r--r--users/Profpatsch/netencode/README.md133
-rw-r--r--users/Profpatsch/netencode/default.nix182
-rw-r--r--users/Profpatsch/netencode/gen.nix73
-rw-r--r--users/Profpatsch/netencode/netencode-mustache.rs52
-rw-r--r--users/Profpatsch/netencode/netencode.cabal25
-rw-r--r--users/Profpatsch/netencode/netencode.rs969
-rw-r--r--users/Profpatsch/netencode/pretty.rs163
-rw-r--r--users/Profpatsch/netstring/README.md18
-rw-r--r--users/Profpatsch/netstring/default.nix64
-rw-r--r--users/Profpatsch/netstring/tests/default.nix64
-rw-r--r--users/Profpatsch/nix-home/README.md7
-rw-r--r--users/Profpatsch/nix-home/default.nix212
-rw-r--r--users/Profpatsch/read-http.nix19
-rw-r--r--users/Profpatsch/read-http.rs249
-rw-r--r--users/Profpatsch/reverse-haskell-deps/README.md3
-rw-r--r--users/Profpatsch/reverse-haskell-deps/ReverseHaskellDeps.hs76
-rw-r--r--users/Profpatsch/reverse-haskell-deps/default.nix32
-rw-r--r--users/Profpatsch/reverse-haskell-deps/reverse-haskell-deps.cabal16
-rw-r--r--users/Profpatsch/shell.nix72
-rw-r--r--users/Profpatsch/sync-abfall-ics-aichach-friedberg/README.md3
-rw-r--r--users/Profpatsch/sync-abfall-ics-aichach-friedberg/default.nix31
-rw-r--r--users/Profpatsch/sync-abfall-ics-aichach-friedberg/ics-to-caldav.dhall139
-rw-r--r--users/Profpatsch/sync-abfall-ics-aichach-friedberg/sync-ics-to-dir.py133
-rw-r--r--users/Profpatsch/tagtime/README.md18
-rw-r--r--users/Profpatsch/toINI.nix79
-rw-r--r--users/Profpatsch/tree-sitter.nix209
-rw-r--r--users/Profpatsch/writers/default.nix120
-rw-r--r--users/Profpatsch/writers/tests/default.nix56
-rw-r--r--users/Profpatsch/ytextr/README.md5
-rw-r--r--users/Profpatsch/ytextr/create-symlink-farm.nix19
-rw-r--r--users/Profpatsch/ytextr/default.nix82
-rw-r--r--users/aaqaishtyaq/OWNERS3
-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/edef/refscan/.gitignore5
-rw-r--r--users/edef/refscan/Cargo.lock7
-rw-r--r--users/edef/refscan/Cargo.lock.license3
-rw-r--r--users/edef/refscan/Cargo.toml10
-rw-r--r--users/edef/refscan/LICENSES/CC0-1.0.txt121
-rw-r--r--users/edef/refscan/LICENSES/MPL-2.0.txt373
-rw-r--r--users/edef/refscan/src/lib.rs154
-rw-r--r--users/edef/refscan/src/main.rs58
-rw-r--r--users/edef/refscan/testdata/.gitignore6
-rwxr-xr-xusers/edef/refscan/testdata/generate.sh8
-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/.envrc2
-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.el1432
-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.el53
-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.el173
-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.el197
-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.el154
-rw-r--r--users/grfn/emacs.d/rust.el42
-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/test-module11
-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.org41
-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.nix73
-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/pkgs/cargo-hakari.nix27
-rw-r--r--users/grfn/pkgs/cargo-nextest.nix27
-rw-r--r--users/grfn/pkgs/notmuch-extract-patch.nix18
-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.nix36
-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/ogopogo.nix75
-rw-r--r--users/grfn/system/home/machines/roswell.nix63
-rw-r--r--users/grfn/system/home/machines/yeren.nix76
-rw-r--r--users/grfn/system/home/modules/.gitignore1
-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.nix125
-rw-r--r--users/grfn/system/home/modules/desktop.nix25
-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.nix34
-rw-r--r--users/grfn/system/home/modules/development/rust.nix45
-rw-r--r--users/grfn/system/home/modules/emacs.nix108
-rw-r--r--users/grfn/system/home/modules/email.nix98
-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.nix392
-rw-r--r--users/grfn/system/home/modules/lib/cloneRepo.nix76
-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.nix18
-rw-r--r--users/grfn/system/home/modules/ptt.nix44
-rwxr-xr-xusers/grfn/system/home/modules/pure.zsh-theme155
-rw-r--r--users/grfn/system/home/modules/rtlsdr.nix23
-rw-r--r--users/grfn/system/home/modules/shell.nix189
-rw-r--r--users/grfn/system/home/modules/tarsnap.nix64
-rw-r--r--users/grfn/system/home/modules/tmux.nix42
-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.nix84
-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.nix46
-rw-r--r--users/grfn/system/system/iso.nix22
-rw-r--r--users/grfn/system/system/machines/bumblebee.nix23
-rw-r--r--users/grfn/system/system/machines/mugwump.nix293
-rw-r--r--users/grfn/system/system/machines/ogopogo.nix84
-rw-r--r--users/grfn/system/system/machines/roswell.nix31
-rw-r--r--users/grfn/system/system/machines/yeren.nix135
-rw-r--r--users/grfn/system/system/modules/common.nix89
-rw-r--r--users/grfn/system/system/modules/desktop.nix19
-rw-r--r--users/grfn/system/system/modules/development.nix15
-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.nix38
-rw-r--r--users/grfn/system/system/modules/wireshark.nix9
-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.yaml157
-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.lock1874
-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.hs822
-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.hs489
-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/j4m3s/OWNERS3
-rw-r--r--users/j4m3s/keys.nix7
-rw-r--r--users/lukegb/OWNERS3
-rw-r--r--users/lukegb/hgext/gerrithook.py63
-rw-r--r--users/lukegb/keys.nix11
-rw-r--r--users/qyliss/OWNERS3
-rw-r--r--users/qyliss/keys.nix8
-rw-r--r--users/riking/OWNERS1
-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/OWNERS1
-rw-r--r--users/sterni/dot-time-man-pages/default.nix70
-rw-r--r--users/sterni/emacs/default.nix110
-rw-r--r--users/sterni/emacs/init.el346
-rw-r--r--users/sterni/emacs/subscriptions.el71
-rw-r--r--users/sterni/exercises/aoc/.gitignore2
-rw-r--r--users/sterni/exercises/aoc/2021/default.nix10
-rwxr-xr-xusers/sterni/exercises/aoc/2021/solutions.bqn484
-rw-r--r--users/sterni/exercises/aoc/2022/.skip-subtree1
-rw-r--r--users/sterni/exercises/aoc/2022/01/1.bqn7
-rw-r--r--users/sterni/exercises/aoc/2022/01/1.k1
-rw-r--r--users/sterni/exercises/aoc/2022/02/2.bqn7
-rw-r--r--users/sterni/exercises/aoc/2022/02/2.k1
-rw-r--r--users/sterni/exercises/aoc/2022/03/3.bqn8
-rw-r--r--users/sterni/exercises/aoc/2022/03/3.k1
-rw-r--r--users/sterni/exercises/aoc/2022/04/4.bqn11
-rw-r--r--users/sterni/exercises/aoc/2022/05/5.bqn18
-rw-r--r--users/sterni/exercises/aoc/2022/06/6.bqn4
-rw-r--r--users/sterni/exercises/aoc/2022/06/6.k1
-rw-r--r--users/sterni/exercises/aoc/2022/07/7.bqn24
-rw-r--r--users/sterni/exercises/aoc/2022/08/8.bqn15
-rw-r--r--users/sterni/exercises/aoc/2022/09/9.bqn17
-rw-r--r--users/sterni/exercises/aoc/2022/10/10.bqn25
-rw-r--r--users/sterni/exercises/aoc/2022/11/11.bqn41
-rw-r--r--users/sterni/exercises/aoc/2022/12/12.bqn16
-rw-r--r--users/sterni/exercises/aoc/2022/13/13.bqn14
-rw-r--r--users/sterni/exercises/aoc/2022/15/15.bqn18
-rw-r--r--users/sterni/exercises/aoc/2022/16/16.k21
-rw-r--r--users/sterni/exercises/aoc/2022/17/17.bqn51
-rw-r--r--users/sterni/exercises/aoc/2022/18/18.bqn14
-rw-r--r--users/sterni/exercises/aoc/2022/20/20.bqn13
-rw-r--r--users/sterni/exercises/aoc/2022/21/21.bqn25
-rw-r--r--users/sterni/exercises/aoc/2022/25/25.bqn4
-rw-r--r--users/sterni/exercises/aoc/2022/25/25.k1
-rw-r--r--users/sterni/exercises/aoc/2022/README.md8
-rw-r--r--users/sterni/exercises/aoc/2022/default.nix53
-rw-r--r--users/sterni/exercises/aoc/lib.bqn18
-rw-r--r--users/sterni/external/flipdot-gschichtler.nix9
-rw-r--r--users/sterni/external/likely-music.nix11
-rw-r--r--users/sterni/external/sources.json26
-rw-r--r--users/sterni/external/sources.nix197
-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.nix8
-rw-r--r--users/sterni/lv/gopher/default.nix8
-rw-r--r--users/sterni/machines/.skip-subtree1
-rw-r--r--users/sterni/machines/default.nix77
-rw-r--r--users/sterni/machines/edwin/default.nix88
-rw-r--r--users/sterni/machines/edwin/gopher.nix19
-rw-r--r--users/sterni/machines/edwin/hardware.nix64
-rw-r--r--users/sterni/machines/edwin/http/code.sterni.lv.nix262
-rw-r--r--users/sterni/machines/edwin/http/fcgiwrap.nix15
-rw-r--r--users/sterni/machines/edwin/http/flipdot.openlab-augsburg.de.nix36
-rw-r--r--users/sterni/machines/edwin/http/likely-music.sterni.lv.nix23
-rw-r--r--users/sterni/machines/edwin/http/nginx.nix28
-rw-r--r--users/sterni/machines/edwin/http/sterni.lv.nix16
-rw-r--r--users/sterni/machines/edwin/minecraft.nix121
-rw-r--r--users/sterni/machines/edwin/network.nix62
-rw-r--r--users/sterni/mblog/.gitignore5
-rw-r--r--users/sterni/mblog/LICENSE674
-rw-r--r--users/sterni/mblog/cli.lisp75
-rw-r--r--users/sterni/mblog/config.lisp31
-rw-r--r--users/sterni/mblog/default.nix47
-rw-r--r--users/sterni/mblog/maildir.lisp20
-rw-r--r--users/sterni/mblog/mblog.lisp147
-rw-r--r--users/sterni/mblog/note.lisp118
-rw-r--r--users/sterni/mblog/packages.lisp64
-rw-r--r--users/sterni/mblog/transformer.lisp130
-rw-r--r--users/sterni/modules/default.nix2
-rw-r--r--users/sterni/modules/disk-checkup.nix25
-rw-r--r--users/sterni/modules/minecraft-fabric.nix497
-rw-r--r--users/sterni/nix/build/buildGopherHole/default.nix109
-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.nix255
-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.nix129
-rw-r--r--users/sterni/nix/int/tests/default.nix459
-rw-r--r--users/sterni/nix/misc/default.nix18
l---------users/sterni/nix/misc/guinea-pig1
-rw-r--r--users/sterni/nix/string/default.nix121
-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/sterni/secrets/default.nix3
-rw-r--r--users/sterni/secrets/minecraft-rcon.age9
-rw-r--r--users/sterni/secrets/secrets.nix15
-rw-r--r--users/sterni/secrets/warteraum-salt.agebin0 -> 530 bytes
-rw-r--r--users/sterni/secrets/warteraum-tokens.age10
-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/aoc2022/day1.rs27
-rw-r--r--users/tazjin/avatar.jpegbin0 -> 81953 bytes
-rw-r--r--users/tazjin/blog/.skip-subtree1
-rw-r--r--users/tazjin/blog/default.nix49
-rw-r--r--users/tazjin/blog/posts.nix64
-rw-r--r--users/tazjin/blog/posts/best-tools.md184
-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/blog/posts/thoughts.md142
-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.el111
-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.el369
-rw-r--r--users/tazjin/emacs/config/init.el308
-rw-r--r--users/tazjin/emacs/config/look-and-feel.el122
-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.nix9
-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/generator-example/.gitignore1
-rw-r--r--users/tazjin/generator-example/Cargo.lock124
-rw-r--r--users/tazjin/generator-example/Cargo.toml9
-rw-r--r--users/tazjin/generator-example/README.md11
-rw-r--r--users/tazjin/generator-example/src/main.rs115
-rw-r--r--users/tazjin/gruber-darker.qss508
-rw-r--r--users/tazjin/hanebuschtag.txt66
-rw-r--r--users/tazjin/home/shared.nix94
-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.nix10
-rw-r--r--users/tazjin/kinesis/README.md10
-rwxr-xr-xusers/tazjin/kinesis/advantage2/qwerty.txt6
-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.nix345
-rw-r--r--users/tazjin/nixos/default.nix10
-rw-r--r--users/tazjin/nixos/frog/default.nix286
-rw-r--r--users/tazjin/nixos/modules/chromium.nix30
-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.nix19
-rw-r--r--users/tazjin/nixos/modules/home-config.nix19
-rw-r--r--users/tazjin/nixos/modules/laptop.nix15
-rw-r--r--users/tazjin/nixos/modules/persistence.nix26
-rw-r--r--users/tazjin/nixos/modules/physical.nix93
-rw-r--r--users/tazjin/nixos/modules/predlozhnik.nix10
-rw-r--r--users/tazjin/nixos/modules/tgsa.nix25
-rw-r--r--users/tazjin/nixos/polyanka/default.nix135
-rw-r--r--users/tazjin/nixos/tverskoy/default.nix170
-rw-r--r--users/tazjin/nixos/zamalek/default.nix88
-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.lock1339
-rw-r--r--users/tazjin/tgsa/Cargo.toml14
-rw-r--r--users/tazjin/tgsa/default.nix14
-rw-r--r--users/tazjin/tgsa/src/main.rs442
-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/.envrc5
-rw-r--r--users/wpcarro/.gitignore35
-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.md47
-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
-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.nix83
-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/.gnupg/crls.d/DIR.txt1
-rw-r--r--users/wpcarro/configs/.gnupg/exported/ownertrust.txt3
-rw-r--r--users/wpcarro/configs/.gnupg/exported/public.asc225
-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.fish44
-rw-r--r--users/wpcarro/dotfiles/default.nix5
-rw-r--r--users/wpcarro/dotfiles/dunstrc53
-rw-r--r--users/wpcarro/dotfiles/gitconfig9
-rw-r--r--users/wpcarro/dotfiles/prompt.fish87
-rw-r--r--users/wpcarro/emacs/.emacs.d/init.el15
-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/buffer.el174
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/clipboard.el40
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/constants.el29
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/display.el103
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/email.el76
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/fonts.el99
-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.el495
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/keyboard.el138
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/modeline.el68
-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/region.el23
-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/ssh.el67
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/window-manager.el228
-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.el51
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-dotnet.el16
-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.el93
-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.el330
-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.el30
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-shell.el31
-rw-r--r--users/wpcarro/emacs/.emacs.d/wpc/wpc-ui.el186
-rw-r--r--users/wpcarro/emacs/AppIcon.icnsbin0 -> 449758 bytes
-rw-r--r--users/wpcarro/emacs/README.md15
-rw-r--r--users/wpcarro/emacs/ci.el44
-rw-r--r--users/wpcarro/emacs/default.nix265
-rw-r--r--users/wpcarro/emacs/elisp-conventions.md20
-rw-r--r--users/wpcarro/emacs/keybindings.md47
-rw-r--r--users/wpcarro/emacs/pkgs/al/al.el227
-rw-r--r--users/wpcarro/emacs/pkgs/al/default.nix28
-rw-r--r--users/wpcarro/emacs/pkgs/al/tests.el53
-rw-r--r--users/wpcarro/emacs/pkgs/bag/bag.el78
-rw-r--r--users/wpcarro/emacs/pkgs/bag/default.nix26
-rw-r--r--users/wpcarro/emacs/pkgs/bag/tests.el32
-rw-r--r--users/wpcarro/emacs/pkgs/bookmark/bookmark.el50
-rw-r--r--users/wpcarro/emacs/pkgs/bookmark/default.nix13
-rw-r--r--users/wpcarro/emacs/pkgs/bytes/bytes.el94
-rw-r--r--users/wpcarro/emacs/pkgs/bytes/default.nix25
-rw-r--r--users/wpcarro/emacs/pkgs/bytes/tests.el18
-rw-r--r--users/wpcarro/emacs/pkgs/cycle/README.md7
-rw-r--r--users/wpcarro/emacs/pkgs/cycle/cycle.el194
-rw-r--r--users/wpcarro/emacs/pkgs/cycle/default.nix36
-rw-r--r--users/wpcarro/emacs/pkgs/cycle/tests.el79
-rw-r--r--users/wpcarro/emacs/pkgs/fs/default.nix29
-rw-r--r--users/wpcarro/emacs/pkgs/fs/fs.el47
-rw-r--r--users/wpcarro/emacs/pkgs/fs/tests.el26
-rw-r--r--users/wpcarro/emacs/pkgs/list/README.md19
-rw-r--r--users/wpcarro/emacs/pkgs/list/default.nix26
-rw-r--r--users/wpcarro/emacs/pkgs/list/list.el219
-rw-r--r--users/wpcarro/emacs/pkgs/list/tests.el107
-rw-r--r--users/wpcarro/emacs/pkgs/macros/default.nix10
-rw-r--r--users/wpcarro/emacs/pkgs/macros/macros.el45
-rw-r--r--users/wpcarro/emacs/pkgs/math/default.nix30
-rw-r--r--users/wpcarro/emacs/pkgs/math/math.el63
-rw-r--r--users/wpcarro/emacs/pkgs/math/tests.el25
-rw-r--r--users/wpcarro/emacs/pkgs/maybe/default.nix24
-rw-r--r--users/wpcarro/emacs/pkgs/maybe/maybe.el54
-rw-r--r--users/wpcarro/emacs/pkgs/maybe/tests.el25
-rw-r--r--users/wpcarro/emacs/pkgs/passage/README.md12
-rw-r--r--users/wpcarro/emacs/pkgs/passage/default.nix12
-rw-r--r--users/wpcarro/emacs/pkgs/passage/passage.el65
-rw-r--r--users/wpcarro/emacs/pkgs/set/default.nix32
-rw-r--r--users/wpcarro/emacs/pkgs/set/set.el116
-rw-r--r--users/wpcarro/emacs/pkgs/set/tests.el69
-rw-r--r--users/wpcarro/emacs/pkgs/string/default.nix27
-rw-r--r--users/wpcarro/emacs/pkgs/string/string.el98
-rw-r--r--users/wpcarro/emacs/pkgs/string/tests.el22
-rw-r--r--users/wpcarro/emacs/pkgs/struct/README.md6
-rw-r--r--users/wpcarro/emacs/pkgs/struct/default.nix29
-rw-r--r--users/wpcarro/emacs/pkgs/struct/struct.el65
-rw-r--r--users/wpcarro/emacs/pkgs/struct/tests.el44
-rw-r--r--users/wpcarro/emacs/pkgs/symbol/default.nix24
-rw-r--r--users/wpcarro/emacs/pkgs/symbol/symbol.el38
-rw-r--r--users/wpcarro/emacs/pkgs/symbol/tests.el22
-rw-r--r--users/wpcarro/emacs/pkgs/theme/default.nix14
-rw-r--r--users/wpcarro/emacs/pkgs/theme/theme.el78
-rw-r--r--users/wpcarro/emacs/pkgs/tuple/default.nix10
-rw-r--r--users/wpcarro/emacs/pkgs/tuple/tuple.el93
-rw-r--r--users/wpcarro/emacs/pkgs/vector/default.nix21
-rw-r--r--users/wpcarro/emacs/pkgs/vector/tests.el20
-rw-r--r--users/wpcarro/emacs/pkgs/vector/vector.el58
-rw-r--r--users/wpcarro/emacs/pkgs/vterm-mgt/README.md17
-rw-r--r--users/wpcarro/emacs/pkgs/vterm-mgt/default.nix19
-rw-r--r--users/wpcarro/emacs/pkgs/vterm-mgt/vterm-mgt.el140
-rw-r--r--users/wpcarro/emacs/pkgs/zle/default.nix10
-rw-r--r--users/wpcarro/emacs/pkgs/zle/zle.el90
-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.nix20
-rw-r--r--users/wpcarro/lib/default.nix5
-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.el61
-rw-r--r--users/wpcarro/nixos/ava/default.nix150
-rw-r--r--users/wpcarro/nixos/default.nix24
-rw-r--r--users/wpcarro/nixos/iso.nix17
-rw-r--r--users/wpcarro/nixos/kyoko/default.nix153
-rw-r--r--users/wpcarro/nixos/kyoko/kyoko.el61
-rw-r--r--users/wpcarro/nixos/marcus/default.nix169
-rw-r--r--users/wpcarro/nixos/marcus/hardware.nix29
-rw-r--r--users/wpcarro/nixos/marcus/marcus.el40
-rw-r--r--users/wpcarro/nixos/modules/.skip-subtree1
-rw-r--r--users/wpcarro/nixos/modules/hadrian-cache.nix17
-rw-r--r--users/wpcarro/nixos/modules/hardware/dell-emc-egw-5200.nix47
-rw-r--r--users/wpcarro/nixos/modules/hardware/nopn.nix53
-rw-r--r--users/wpcarro/nixos/modules/laptop.nix15
-rw-r--r--users/wpcarro/nixos/modules/nginx.nix45
-rw-r--r--users/wpcarro/nixos/tarasco/default.nix144
-rw-r--r--users/wpcarro/nixos/tarasco/tarasco.el61
-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/compiler/.envrc3
-rw-r--r--users/wpcarro/scratch/compiler/.gitignore5
-rw-r--r--users/wpcarro/scratch/compiler/debug.ml66
-rw-r--r--users/wpcarro/scratch/compiler/expr_parser.ml187
-rw-r--r--users/wpcarro/scratch/compiler/inference.ml183
-rw-r--r--users/wpcarro/scratch/compiler/parser.ml47
-rw-r--r--users/wpcarro/scratch/compiler/prettify.ml9
-rw-r--r--users/wpcarro/scratch/compiler/register_vm.ml129
-rw-r--r--users/wpcarro/scratch/compiler/register_vm.py161
-rw-r--r--users/wpcarro/scratch/compiler/shell.nix9
-rw-r--r--users/wpcarro/scratch/compiler/tests.ml43
-rw-r--r--users/wpcarro/scratch/compiler/type_parser.ml104
-rw-r--r--users/wpcarro/scratch/compiler/types.ml31
-rw-r--r--users/wpcarro/scratch/compiler/vec.ml127
-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/rust/.gitignore1
-rw-r--r--users/wpcarro/scratch/rust/Cargo.lock89
-rw-r--r--users/wpcarro/scratch/rust/Cargo.toml8
-rw-r--r--users/wpcarro/scratch/rust/README.md11
-rw-r--r--users/wpcarro/scratch/rust/shell.nix7
-rw-r--r--users/wpcarro/scratch/rust/src/display/mod.rs13
-rw-r--r--users/wpcarro/scratch/rust/src/json/mod.rs81
-rw-r--r--users/wpcarro/scratch/rust/src/main.rs15
-rw-r--r--users/wpcarro/scratch/rust/src/rc/mod.rs12
-rw-r--r--users/wpcarro/scratch/rust/src/stdin/mod.rs22
-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/slx.js/.gitignore3
-rw-r--r--users/wpcarro/slx.js/README.md55
-rw-r--r--users/wpcarro/slx.js/default.nix11
-rw-r--r--users/wpcarro/slx.js/index.html13
-rw-r--r--users/wpcarro/slx.js/index.js455
-rw-r--r--users/wpcarro/slx.js/package.json14
-rw-r--r--users/wpcarro/slx.js/tests.js68
-rw-r--r--users/wpcarro/slx.js/yarn.lock1495
-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/README.md41
-rw-r--r--users/wpcarro/tools/monzo_ynab/auth.go101
-rw-r--r--users/wpcarro/tools/monzo_ynab/main.go48
-rw-r--r--users/wpcarro/tools/monzo_ynab/monzo/client.go52
-rw-r--r--users/wpcarro/tools/monzo_ynab/monzo/serde.go72
-rw-r--r--users/wpcarro/tools/monzo_ynab/requests.txt80
-rw-r--r--users/wpcarro/tools/monzo_ynab/tokens.go279
-rw-r--r--users/wpcarro/tools/monzo_ynab/ynab/client.go9
-rw-r--r--users/wpcarro/tools/monzo_ynab/ynab/serde.go44
-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/systemd-shell/default.nix8
-rw-r--r--users/wpcarro/tools/systemd-shell/setup.py9
-rw-r--r--users/wpcarro/tools/systemd-shell/systemd-shell36
-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/tools/wpcarro-deps.nix8
-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.nix47
-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.nix116
-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/csharp-unused-variables.md38
-rw-r--r--users/wpcarro/website/blog/posts/git-filter-repo-note.md59
-rw-r--r--users/wpcarro/website/blog/posts/git-rev-refs.md85
-rw-r--r--users/wpcarro/website/blog/posts/importing-subtrees.md147
-rw-r--r--users/wpcarro/website/blog/posts/nginx-curl-note.md5
-rw-r--r--users/wpcarro/website/blog/posts/nix-env-note.md33
-rw-r--r--users/wpcarro/website/blog/posts/nix-shell-note.md50
-rw-r--r--users/wpcarro/website/blog/posts/nixos-disk-full-note.md113
-rw-r--r--users/wpcarro/website/blog/posts/quassel-google-vm.md34
-rw-r--r--users/wpcarro/website/blog/posts/restic.md91
-rw-r--r--users/wpcarro/website/blog/posts/send-mail-as-2fa.md43
-rw-r--r--users/wpcarro/website/blog/posts/ssh-oddities.md59
-rw-r--r--users/wpcarro/website/blog/posts/tcp-tunneling-note.md63
-rw-r--r--users/wpcarro/website/blog/posts/tee-time.md16
-rw-r--r--users/wpcarro/website/default.nix77
-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.html96
-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/ynabsql/dataviz/.gitignore5
-rw-r--r--users/wpcarro/ynabsql/dataviz/.parcelrc3
l---------users/wpcarro/ynabsql/dataviz/cdn1
-rw-r--r--users/wpcarro/ynabsql/dataviz/components.jsx1256
-rw-r--r--users/wpcarro/ynabsql/dataviz/index.html19
-rw-r--r--users/wpcarro/ynabsql/dataviz/package.json15
-rw-r--r--users/wpcarro/ynabsql/dataviz/yarn.lock1540
-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.md31
-rw-r--r--views/default.nix26
-rw-r--r--views/kit/README.md26
-rw-r--r--views/kit/buildkite.yml16
-rw-r--r--views/kit/default.nix33
-rw-r--r--views/kit/workspace.josh14
-rw-r--r--views/tvix/buildkite.yml9
-rw-r--r--views/tvix/default.nix24
-rw-r--r--views/tvix/workspace.josh5
-rw-r--r--web/atom-feed/default.nix153
-rw-r--r--web/atward/.gitignore3
-rw-r--r--web/atward/Cargo.lock814
-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.nix66
-rw-r--r--web/blog/fragments.nix95
-rw-r--r--web/bubblegum/OWNERS1
-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/cgit-tvl/default.nix73
-rw-r--r--web/cgit-tvl/thttpd_cgi_idx.patch13
-rw-r--r--web/converse/.gitignore3
-rw-r--r--web/converse/Cargo.lock3404
-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/inbox.nix81
-rw-r--r--web/panettone/.envrc1
-rw-r--r--web/panettone/.gitignore1
-rw-r--r--web/panettone/OWNERS3
-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.css198
-rw-r--r--web/todolist/default.nix116
-rw-r--r--web/todolist/extract-todos.jq30
-rw-r--r--web/tvl/blog/default.nix27
-rw-r--r--web/tvl/blog/rewriting-nix.md90
-rw-r--r--web/tvl/blog/tvix-status-202209.md165
-rw-r--r--web/tvl/default.nix141
-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.dot174
3775 files changed, 497027 insertions, 698 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 000000000000..1f15539fb2d4
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,18 @@
+# Create a gcroot that keeps all third_party.sources alive
+nix-build --out-link .gcroots/sources -E '
+with import ./. {};
+third_party.nixpkgs.writeText "depot-3p-sources.txt" (
+  toString (
+    builtins.map (s: s.outPath or null) (
+      builtins.attrValues third_party.sources
+    )
+  )
+)'
+
+# Configure the local PATH to contain tools which are fetched ad-hoc
+# from Nix.
+out=$(nix-build -A tools.depot-deps --out-link .gcroots/depot-deps)
+PATH_add "$out/bin"
+
+watch_file tools/depot-deps.nix
+watch_file third_party/sources/sources.json
diff --git a/.gcroots/.skip-subtree b/.gcroots/.skip-subtree
new file mode 100644
index 000000000000..8c3e8d06db93
--- /dev/null
+++ b/.gcroots/.skip-subtree
@@ -0,0 +1 @@
+these are just symlinks to prevent Nix from gc-ing paths we'd like to keep
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 000000000000..ca28d690e803
--- /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
index 9e4b0ee5b48e..9066757e58e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,10 @@
-*.elc
-*-pkg.el
-*-autoloads.el
+# Ignore the garbage folder, in which I slowly assemble a bunch of
+# trash locally that might be valuable in the future.
+garbage/
+
+# Nix gcroot symlinks created by .envrc
+/.gcroots/*
+
+# Ignore Nix result symlinks
+result
+result-*
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 000000000000..6b8710a711f3
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1 @@
+.git
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 000000000000..06e6c9a44997
--- /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 000000000000..176209130023
--- /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 000000000000..19da286ee345
--- /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 000000000000..6c19dfbda4bc
--- /dev/null
+++ b/.rgignore
@@ -0,0 +1 @@
+third_party/git
diff --git a/LICENSE b/LICENSE
index 9cecc1d4669e..af4c6ae3cfa2 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,674 +1,22 @@
-                    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>.
+The MIT License (MIT)
+
+Copyright (c) 2019 Vincent Ambo
+Copyright (c) 2020-2023 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 000000000000..cdf20d65b0d6
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,6 @@
+set noparent
+
+tazjin
+lukegb
+grfn
+sterni
diff --git a/README.md b/README.md
index 6d7e0dd1ff17..986b7362751e 100644
--- a/README.md
+++ b/README.md
@@ -1,21 +1,124 @@
-# 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).
+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 000000000000..c6089c4bfd79
--- /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.gen.yaml b/buf.gen.yaml
new file mode 100644
index 000000000000..4ed18be22e62
--- /dev/null
+++ b/buf.gen.yaml
@@ -0,0 +1,8 @@
+version: v1
+plugins:
+ - name: go
+   out: .
+   opt: paths=source_relative
+ - name: go-grpc
+   out: .
+   opt: paths=source_relative
\ No newline at end of file
diff --git a/buf.yaml b/buf.yaml
new file mode 100644
index 000000000000..ab2028f40e07
--- /dev/null
+++ b/buf.yaml
@@ -0,0 +1,12 @@
+version: v1
+
+lint:
+  allow_comment_ignores: true
+  use:
+    - BASIC
+    - FILE_LOWER_SNAKE_CASE
+  except:
+    - ENUM_VALUE_UPPER_SNAKE_CASE
+    - PACKAGE_DIRECTORY_MATCH
+  ignore:
+    - nix/buildGo
diff --git a/corp/LICENSE b/corp/LICENSE
new file mode 100644
index 000000000000..6ae339dea33f
--- /dev/null
+++ b/corp/LICENSE
@@ -0,0 +1,4 @@
+Copyright 2021-2023 ООО ТВЛ
+
+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 000000000000..e99d7151f3b7
--- /dev/null
+++ b/corp/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+group:tvl-employees
diff --git a/corp/ops/.envrc b/corp/ops/.envrc
new file mode 100644
index 000000000000..26049cf426a4
--- /dev/null
+++ b/corp/ops/.envrc
@@ -0,0 +1,4 @@
+out=$(nix-build ../.. -A corp.ops.deps --out-link ../../.gcroots/corp-deps)
+PATH_add "$out/bin"
+
+watch_file default.nix
diff --git a/corp/ops/.gitignore b/corp/ops/.gitignore
new file mode 100644
index 000000000000..5def054d764c
--- /dev/null
+++ b/corp/ops/.gitignore
@@ -0,0 +1,4 @@
+.terraform
+.terraform.lock.hcl
+terraform.tfstate
+terraform.tfstate.backup
diff --git a/corp/ops/default.nix b/corp/ops/default.nix
new file mode 100644
index 000000000000..59d63bd5101b
--- /dev/null
+++ b/corp/ops/default.nix
@@ -0,0 +1,46 @@
+{ depot, lib, pkgs, ... }:
+
+depot.nix.readTree.drvTargets rec {
+  # Provide a Terraform wrapper with Yandex Cloud support.
+  terraform = pkgs.terraform.withPlugins (p: [
+    p.yandex
+  ]);
+
+  validate = depot.tools.checks.validateTerraform {
+    inherit terraform;
+    name = "corp";
+    src = lib.cleanSource ./.;
+  };
+
+  # Yandex Cloud CLI
+  yc-cli = pkgs.stdenv.mkDerivation rec {
+    pname = "yc-cli";
+    version = "0.106.0";
+
+    src = pkgs.fetchurl {
+      url = "https://storage.yandexcloud.net/yandexcloud-yc/release/${version}/linux/amd64/yc";
+      sha256 = "sha256:1f7fq9rlihz91ld1vdjj9vq9ssq1ls031jin4zisxv75rcdpslh3";
+    };
+
+    phases = [ "installPhase" ];
+    installPhase = "install -D $src $out/bin/yc";
+  };
+
+  deps = depot.tools.depot-deps.overrideDeps {
+    tf-yandex = {
+      attr = "corp.ops.terraform";
+      cmd = "terraform";
+    };
+
+    yc.attr = "corp.ops.yc-cli";
+  };
+
+  # Base image for Yandex VMs.
+  yandex-base-image = (depot.third_party.nixos {
+    configuration = { ... }: {
+      imports = [
+        (depot.path.origSrc + ("/corp/ops/modules/yandex-cloud.nix"))
+      ];
+    };
+  }).config.system.build.yandexCloudImage;
+}
diff --git a/corp/ops/modules/.skip-tree b/corp/ops/modules/.skip-tree
new file mode 100644
index 000000000000..a6f528167f00
--- /dev/null
+++ b/corp/ops/modules/.skip-tree
@@ -0,0 +1 @@
+Only NixOS modules here.
diff --git a/corp/ops/modules/yandex-cloud.nix b/corp/ops/modules/yandex-cloud.nix
new file mode 100644
index 000000000000..cca81bc0ca5e
--- /dev/null
+++ b/corp/ops/modules/yandex-cloud.nix
@@ -0,0 +1,79 @@
+# Profile for virtual machines on Yandex Cloud, intended for disk
+# images.
+#
+# https://cloud.yandex.com/en/docs/compute/operations/image-create/custom-image
+#
+# TODO(tazjin): Upstream to nixpkgs once it works well.
+{ config, lib, pkgs, modulesPath, ... }:
+
+let
+  cfg = config.virtualisation.yandexCloud;
+
+  # Kernel modules required for interacting with the hypervisor. These
+  # must be available during stage 1 boot and during normal operation,
+  # as disks and network do not work without them.
+  modules = [
+    "virtio-net"
+    "virtio-blk"
+    "virtio-pci"
+    "virtiofs"
+  ];
+in
+{
+  imports = [
+    "${modulesPath}/profiles/headless.nix"
+  ];
+
+  options = {
+    virtualisation.yandexCloud.rootPartitionUuid = with lib; mkOption {
+      type = types.str;
+      default = "C55A5EE2-E5FA-485C-B3AE-CC928429AB6B";
+
+      description = ''
+        UUID to use for the root partition of the disk image. Yandex
+        Cloud requires that root partitions are mounted by UUID.
+
+        Most users do not need to set this to a non-default value.
+      '';
+    };
+  };
+
+  config = {
+    fileSystems."/" = {
+      device = "/dev/disk/by-uuid/${lib.toLower cfg.rootPartitionUuid}";
+      fsType = "ext4";
+      autoResize = true;
+    };
+
+    boot = {
+      loader.grub.device = "/dev/vda";
+
+      initrd.kernelModules = modules;
+      kernelModules = modules;
+      kernelParams = [
+        # Enable support for the serial console
+        "console=ttyS0"
+      ];
+
+      growPartition = true;
+    };
+
+    environment.etc.securetty = {
+      text = "ttyS0";
+      mode = "0644";
+    };
+
+    systemd.services."serial-getty@ttyS0".enable = true;
+
+    services.openssh.enable = true;
+    services.cloud-init.enable = true;
+
+    system.build.yandexCloudImage = import (pkgs.path + "/nixos/lib/make-disk-image.nix") {
+      inherit lib config pkgs;
+      additionalSpace = "128M";
+      format = "qcow2";
+      partitionTableType = "legacy+gpt";
+      rootGPUID = cfg.rootPartitionUuid;
+    };
+  };
+}
diff --git a/corp/ops/yandex/creds.fish b/corp/ops/yandex/creds.fish
new file mode 100644
index 000000000000..2985b2880864
--- /dev/null
+++ b/corp/ops/yandex/creds.fish
@@ -0,0 +1,5 @@
+export YC_TOKEN=(yc iam create-token)
+export YC_CLOUD_ID=(yc config get cloud-id)
+export YC_FOLDER_ID=(yc config get folder-id)
+export AWS_ACCESS_KEY_ID="YCAJE6eRLY8Az-9kveNRtz4sh"
+export AWS_SECRET_ACCESS_KEY=(yc kms symmetric-crypto decrypt --name tvl-credentials --cloud-id b1ggu5m1btue982app12 --folder-name default --ciphertext-file encrypted-state-secret.key --plaintext-file /dev/stdout | head -n1)
diff --git a/corp/ops/yandex/encrypted-state-secret.key b/corp/ops/yandex/encrypted-state-secret.key
new file mode 100644
index 000000000000..0d07158f2f01
--- /dev/null
+++ b/corp/ops/yandex/encrypted-state-secret.key
Binary files differdiff --git a/corp/ops/yandex/main.tf b/corp/ops/yandex/main.tf
new file mode 100644
index 000000000000..cd8fa6e4cc67
--- /dev/null
+++ b/corp/ops/yandex/main.tf
@@ -0,0 +1,70 @@
+# Terraform configuration for TVL corp infrastructure (on Yandex
+# Cloud).
+
+terraform {
+  required_providers {
+    yandex = {
+      source = "yandex-cloud/yandex"
+    }
+  }
+
+  # Credentials need to be sourced from creds.fish
+  backend "s3" {
+    endpoint = "storage.yandexcloud.net"
+    bucket   = "su-tvl-terraform-state"
+    region   = "ru-central1"
+    key      = "corp/ops/terraform.tfstate"
+
+    skip_region_validation      = true
+    skip_credentials_validation = true
+  }
+}
+
+provider "yandex" {
+  zone = "ru-central1-b"
+}
+
+locals {
+  tvl_cloud_id  = "b1ggu5m1btue982app12"
+  tvl_folder_id = "b1gmbeqt9o5kbl7rclln"
+  rih_cloud_id  = "b1glccvcqggi2ruibgvt"
+  rih_folder_id = "b1gsavcrsjn059d1sbh9"
+}
+
+# Storage state bucket configuration
+
+resource "yandex_iam_service_account" "tf_state_sa" {
+  folder_id = local.tvl_folder_id
+  name      = "terraform-state"
+}
+
+resource "yandex_resourcemanager_folder_iam_member" "tf_state_sa_storage" {
+  folder_id = local.tvl_folder_id
+  role      = "storage.editor"
+  member    = "serviceAccount:${yandex_iam_service_account.tf_state_sa.id}"
+}
+
+resource "yandex_iam_service_account_static_access_key" "tf_state_sa_key" {
+  service_account_id = yandex_iam_service_account.tf_state_sa.id
+  description        = "Static access key for Terraform state"
+}
+
+resource "yandex_storage_bucket" "tf_state" {
+  access_key = yandex_iam_service_account_static_access_key.tf_state_sa_key.access_key
+  secret_key = yandex_iam_service_account_static_access_key.tf_state_sa_key.secret_key
+  bucket     = "su-tvl-terraform-state"
+}
+
+# Secret management configuration
+
+resource "yandex_kms_symmetric_key" "tvl_credentials_key" {
+  name              = "tvl-credentials"
+  folder_id         = local.tvl_folder_id
+  default_algorithm = "AES_256"
+  rotation_period   = "2160h" # 90 days
+}
+
+resource "yandex_kms_secret_ciphertext" "tf_state_key" {
+  key_id    = yandex_kms_symmetric_key.tvl_credentials_key.id
+  plaintext = yandex_iam_service_account_static_access_key.tf_state_sa_key.secret_key
+}
diff --git a/corp/ops/yandex/rih.tf b/corp/ops/yandex/rih.tf
new file mode 100644
index 000000000000..de51ad738e80
--- /dev/null
+++ b/corp/ops/yandex/rih.tf
@@ -0,0 +1,69 @@
+# Deployment configuration for russiaishiring.com
+#
+# The frontend of the page is served from a storage bucket, the
+# backend runs in a container.
+
+resource "yandex_dns_zone" "russiaishiring_com" {
+  name      = "russiaishiring-com"
+  zone      = "russiaishiring.com."
+  public    = true
+  folder_id = local.rih_folder_id
+}
+
+resource "yandex_iam_service_account" "rih_storage_sa" {
+  name      = "rih-storage-sa"
+  folder_id = local.rih_folder_id
+}
+
+resource "yandex_resourcemanager_folder_iam_member" "rih_sa_storage_editor" {
+  folder_id = local.rih_folder_id
+  role      = "storage.editor"
+  member    = "serviceAccount:${yandex_iam_service_account.rih_storage_sa.id}"
+}
+
+resource "yandex_iam_service_account_static_access_key" "rih_sa_static_key" {
+  service_account_id = yandex_iam_service_account.rih_storage_sa.id
+  description        = "RIH bucket access key"
+}
+
+resource "yandex_storage_bucket" "rih_storage_bucket" {
+  access_key = yandex_iam_service_account_static_access_key.rih_sa_static_key.access_key
+  secret_key = yandex_iam_service_account_static_access_key.rih_sa_static_key.secret_key
+  bucket     = "russiaishiring.com"
+  folder_id  = local.rih_folder_id
+  acl        = "public-read"
+
+  https {
+    certificate_id = yandex_cm_certificate.russiaishiring_com.id
+  }
+
+  website {
+    index_document = "index.html"
+  }
+}
+
+resource "yandex_cm_certificate" "russiaishiring_com" {
+  folder_id = local.rih_folder_id
+  name      = "russiaishiring-com"
+  domains   = ["russiaishiring.com"]
+
+  managed {
+    challenge_type = "DNS_CNAME"
+  }
+}
+
+resource "yandex_dns_recordset" "acme_russiaishiring_com" {
+  zone_id = yandex_dns_zone.russiaishiring_com.id
+  name    = yandex_cm_certificate.russiaishiring_com.challenges[0].dns_name
+  type    = yandex_cm_certificate.russiaishiring_com.challenges[0].dns_type
+  data    = [yandex_cm_certificate.russiaishiring_com.challenges[0].dns_value]
+  ttl     = 60
+}
+
+resource "yandex_dns_recordset" "aname_russiaishiring_com" {
+  zone_id = yandex_dns_zone.russiaishiring_com.id
+  name    = "russiaishiring.com."
+  type    = "ANAME"
+  data    = ["russiaishiring.com.website.yandexcloud.net"]
+  ttl     = 600
+}
diff --git a/corp/rih/.gitignore b/corp/rih/.gitignore
new file mode 100644
index 000000000000..64476c05e05d
--- /dev/null
+++ b/corp/rih/.gitignore
@@ -0,0 +1,3 @@
+result
+target
+dist
diff --git a/corp/rih/Cargo.lock b/corp/rih/Cargo.lock
new file mode 100644
index 000000000000..3ed11c4ef221
--- /dev/null
+++ b/corp/rih/Cargo.lock
@@ -0,0 +1,1812 @@
+# 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 = "aho-corasick"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is-terminal",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anymap2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+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 = "boolinator"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
+
+[[package]]
+name = "bumpalo"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "clap"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "bitflags",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "191d9573962933b4027f932c600cd252ce27a8ad5979418fe78e43c07996f27b"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "comrak"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
+dependencies = [
+ "clap",
+ "entities",
+ "memchr",
+ "once_cell",
+ "regex",
+ "shell-words",
+ "slug",
+ "syntect",
+ "typed-arena",
+ "unicode_categories",
+ "xdg",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "csv"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad"
+dependencies = [
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "deunicode"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+
+[[package]]
+name = "dirs-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fancy-regex"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf"
+dependencies = [
+ "bit-set",
+ "regex",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+dependencies = [
+ "crc32fast",
+ "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.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fuzzy-matcher"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
+dependencies = [
+ "thread_local",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a4bef6b277b3ab073253d4bca60761240cf8d6998f4bd142211957b69a61b20"
+dependencies = [
+ "gloo-console",
+ "gloo-dialogs",
+ "gloo-events",
+ "gloo-file",
+ "gloo-history",
+ "gloo-net",
+ "gloo-render",
+ "gloo-storage",
+ "gloo-timers",
+ "gloo-utils",
+ "gloo-worker",
+]
+
+[[package]]
+name = "gloo-console"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-dialogs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-events"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
+dependencies = [
+ "futures-channel",
+ "gloo-events",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-history"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd451019e0b7a2b8a7a7b23e74916601abf1135c54664e57ff71dcc26dfcdeb7"
+dependencies = [
+ "gloo-events",
+ "gloo-utils",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_urlencoded",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9902a044653b26b99f7e3693a42f171312d9be8b26b5697bd1e43ad1f8a35e10"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-render"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-worker"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a"
+dependencies = [
+ "anymap2",
+ "bincode",
+ "gloo-console",
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "home"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "implicit-clone"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40fc102e70475c320b185cd18c1e48bba2d7210b63970a4d581ef903e4368ef7"
+dependencies = [
+ "indexmap",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+
+[[package]]
+name = "js-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+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.141"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "onig"
+version = "6.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
+dependencies = [
+ "bitflags",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "phf"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[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 = "pinned"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b"
+dependencies = [
+ "futures",
+ "rustversion",
+ "thiserror",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
+[[package]]
+name = "plist"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590"
+dependencies = [
+ "base64",
+ "indexmap",
+ "line-wrap",
+ "quick-xml",
+ "serde",
+ "time",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "prettytable-rs"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
+dependencies = [
+ "csv",
+ "encode_unicode",
+ "is-terminal",
+ "lazy_static",
+ "term",
+ "unicode-width",
+]
+
+[[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 1.0.109",
+ "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.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prokio"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488"
+dependencies = [
+ "futures",
+ "gloo",
+ "num_cpus",
+ "once_cell",
+ "pin-project",
+ "pinned",
+ "tokio",
+ "tokio-stream",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "quick-xml"
+version = "0.28.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+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.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.7.2",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
+
+[[package]]
+name = "rih"
+version = "0.1.0"
+dependencies = [
+ "fuzzy-matcher",
+ "getrandom",
+ "gloo",
+ "rand",
+ "rust_iso3166",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "static_markdown",
+ "wasm-bindgen",
+ "web-sys",
+ "yew",
+ "yew-router",
+]
+
+[[package]]
+name = "route-recognizer"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
+
+[[package]]
+name = "rust_iso3166"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e19f23f46e5d2f3f1e917ecf5cc988e23ba7fc2a2ce3ef09cb17033842d0ef8"
+dependencies = [
+ "js-sys",
+ "phf",
+ "prettytable-rs",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
+
+[[package]]
+name = "ryu"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
+[[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.160"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.160"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
+
+[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "slug"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+dependencies = [
+ "deunicode",
+]
+
+[[package]]
+name = "static_markdown"
+version = "1.0.0"
+dependencies = [
+ "comrak",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syntect"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8"
+dependencies = [
+ "bincode",
+ "bitflags",
+ "fancy-regex",
+ "flate2",
+ "fnv",
+ "lazy_static",
+ "once_cell",
+ "onig",
+ "plist",
+ "regex-syntax 0.6.29",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "thiserror",
+ "walkdir",
+ "yaml-rust",
+]
+
+[[package]]
+name = "term"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
+dependencies = [
+ "dirs-next",
+ "rustversion",
+ "winapi",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237"
+dependencies = [
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "time"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
+dependencies = [
+ "itoa",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
+
+[[package]]
+name = "time-macros"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "tokio"
+version = "1.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001"
+dependencies = [
+ "autocfg",
+ "pin-project-lite",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typed-arena"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[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.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "web-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+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.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "xdg"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee"
+dependencies = [
+ "home",
+]
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
+[[package]]
+name = "yew"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dbecfe44343b70cc2932c3eb445425969ae21754a8ab3a0966981c1cf7af1cc"
+dependencies = [
+ "console_error_panic_hook",
+ "futures",
+ "gloo",
+ "implicit-clone",
+ "indexmap",
+ "js-sys",
+ "prokio",
+ "rustversion",
+ "serde",
+ "slab",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "yew-macro",
+]
+
+[[package]]
+name = "yew-macro"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b64c253c1d401f1ea868ca9988db63958cfa15a69f739101f338d6f05eea8301"
+dependencies = [
+ "boolinator",
+ "once_cell",
+ "prettyplease",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "yew-router"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "426ee0486d2572a6c5e39fbdbc48b58d59bb555f3326f54631025266cf04146e"
+dependencies = [
+ "gloo",
+ "js-sys",
+ "route-recognizer",
+ "serde",
+ "serde_urlencoded",
+ "tracing",
+ "wasm-bindgen",
+ "web-sys",
+ "yew",
+ "yew-router-macro",
+]
+
+[[package]]
+name = "yew-router-macro"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b249cdb39e0cddaf0644dedc781854524374664793479fdc01e6a65d6e6ae3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
diff --git a/corp/rih/Cargo.toml b/corp/rih/Cargo.toml
new file mode 100644
index 000000000000..16221b7a55c8
--- /dev/null
+++ b/corp/rih/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+version = "0.1.0"
+name = "rih"
+authors = [ "Vincent Ambo <tazjin@tvl.su>" ]
+license = "Proprietary"
+edition = "2021"
+
+[dependencies]
+fuzzy-matcher = "0.3.7"
+gloo = "0.8"
+rust_iso3166 = "0.1.10"
+serde_json = "1.0"
+serde_urlencoded = "*" # pinned by yew
+yew = { version = "0.20", features = ["csr"] }
+yew-router = "0.17"
+rand = "0.8"
+getrandom = { version = "0.2", features = ["js"] }
+
+# needs to be in sync with nixpkgs
+wasm-bindgen = "= 0.2.84"
+
+[dependencies.serde]
+version = "*" # pinned by yew
+features = [ "derive" ]
+
+[dependencies.web-sys]
+version = "*" # pinned by yew
+features = [ "HtmlDetailsElement" ]
+
+[dependencies.static_markdown]
+path = "./static-markdown"
+
+[profile.release]
+lto = true
+opt-level = 'z'
+codegen-units = 1
+
+[package.metadata.wasm-pack.profile.release]
+wasm-opt = ['-Os']
diff --git a/corp/rih/README.md b/corp/rih/README.md
new file mode 100644
index 000000000000..e44d0f2a58e0
--- /dev/null
+++ b/corp/rih/README.md
@@ -0,0 +1,3 @@
+Implementation of russiaishiring.com.
+
+This is a corporate TVL project, see `//corp/LICENSE`.
diff --git a/corp/rih/default.nix b/corp/rih/default.nix
new file mode 100644
index 000000000000..24bbde09b975
--- /dev/null
+++ b/corp/rih/default.nix
@@ -0,0 +1,52 @@
+{ lib, pkgs, ... }:
+
+let
+  wasmRust = pkgs.rust-bin.stable.latest.default.override {
+    targets = [ "wasm32-unknown-unknown" ];
+  };
+
+  cargoToml = with builtins; fromTOML (readFile ./Cargo.toml);
+
+  wasmBindgenMatch =
+    cargoToml.dependencies.wasm-bindgen == "= ${pkgs.wasm-bindgen-cli.version}";
+
+  assertWasmBindgen = assert (lib.assertMsg wasmBindgenMatch ''
+    Due to instability in the Rust WASM ecosystem, the trunk build
+    tool enforces that the Cargo-dependency version of `wasm-bindgen`
+    MUST match the version of the CLI supplied in the environment.
+
+    This can get out of sync when nixpkgs is updated. To resolve it,
+    wasm-bindgen must be bumped in the Cargo.toml file and cargo needs
+    to be run to resolve the dependencies.
+
+    Versions of `wasm-bindgen` in Cargo.toml:
+
+      Expected: '= ${pkgs.wasm-bindgen-cli.version}'
+      Actual:   '${cargoToml.dependencies.wasm-bindgen}'
+  ''); pkgs.wasm-bindgen-cli;
+
+  deps = with pkgs; [
+    binaryen
+    sass
+    wasmRust
+    trunk
+    assertWasmBindgen
+  ];
+
+in
+pkgs.rustPlatform.buildRustPackage rec {
+  pname = "rih-frontend";
+  version = "canon";
+  src = lib.cleanSource ./.;
+  cargoLock.lockFile = ./Cargo.lock;
+
+  buildPhase = ''
+    export PATH=${lib.makeBinPath deps}:$PATH
+    mkdir home
+    export HOME=$PWD/.home
+    env
+    trunk build --release -d $out
+  '';
+
+  dontInstall = true;
+}
diff --git a/corp/rih/index.css b/corp/rih/index.css
new file mode 100644
index 000000000000..e71a9c8b0c03
--- /dev/null
+++ b/corp/rih/index.css
@@ -0,0 +1,62 @@
+.b-section-divider {
+    width: 100%;
+    height: 3rem;
+    background-color: rgba(0, 0, 0, .1);
+    border: solid rgba(0, 0, 0, .15);
+    border-width: 1px 0;
+    box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
+}
+
+
+/* .btn-light, */
+/* .btn-light:hover, */
+/* .btn-light:focus { */
+/*   color: #333; */
+/*   text-shadow: none; /\* Prevent inheritance from `body` *\/ */
+/* } */
+
+/* body { */
+/*   text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); */
+/*   box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); */
+/* } */
+
+/* .cover-container { */
+/*   max-width: 42em; */
+/* } */
+
+/* .nav-masthead .nav-link { */
+/*   color: rgba(255, 255, 255, .5); */
+/*   border-bottom: .25rem solid transparent; */
+/* } */
+
+/* .nav-masthead .nav-link:hover, */
+/* .nav-masthead .nav-link:focus { */
+/*   border-bottom-color: rgba(255, 255, 255, .25); */
+/* } */
+
+/* .nav-masthead .nav-link + .nav-link { */
+/*   margin-left: 1rem; */
+/* } */
+
+/* .nav-masthead .active { */
+/*   color: #fff; */
+/*   border-bottom-color: #fff; */
+/* } */
+
+/* body { */
+/*   font-family: Arial, sans-serif; */
+/*   line-height: 1.6; */
+/*   margin: 20px; */
+/*   color: #333; */
+/* } */
+
+/* header { */
+/*   text-align: center; */
+/*   margin-bottom: 30px; */
+/* } */
+
+/* h1 { */
+/*   color: #0039a6; */
+/* } */
+
+/* .intro */
diff --git a/corp/rih/index.html b/corp/rih/index.html
new file mode 100644
index 000000000000..b7d1803f1139
--- /dev/null
+++ b/corp/rih/index.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html lang="en" data-bs-theme="auto">
+  <head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="theme-color" content="#712cf9">
+    <title>Russia is Hiring</title>
+    <link data-trunk rel="inline" href="index.css">
+    <link data-trunk rel="copy-file" href="rih-logo.png">
+
+    <!-- Bootstrap -->
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
+  </head>
+  <body>
+  </body>
+</html>
diff --git a/corp/rih/rih-logo.png b/corp/rih/rih-logo.png
new file mode 100644
index 000000000000..5d12843ede6b
--- /dev/null
+++ b/corp/rih/rih-logo.png
Binary files differdiff --git a/corp/rih/src/home.html b/corp/rih/src/home.html
new file mode 100644
index 000000000000..2098e9a6dd6f
--- /dev/null
+++ b/corp/rih/src/home.html
@@ -0,0 +1,178 @@
+html! {
+<main>
+  <div class="container px-4 pt-5 my-5 text-center">
+    <div class="row">
+      <div class="col-7 ms-auto">
+        <h1 class="display-5 fw-bold text-body-emphasis">{"Russia is Hiring"}</h1>
+        <p class="lead my-4">
+          {"Are you an IT-specialist on the hunt for a job? Well, "}
+          <a href="https://archive.is/SAONj" class="text-black">{"times are tough"}</a>
+          {" in Western countries at the moment. Meanwhile tech is booming in Russia, and national support programs make life as an IT-specialist very comfortable. Why not look East?"}
+        </p>
+        <p class="lead mb-4">{"We can help you find an employer in Russia, sort out the formalities and get you started. Sign up and tell us a bit about your profile, or read on below about the benefits of life in Russia."}</p>
+
+        <div class="d-grid gap-2 d-sm-flex justify-content-sm-center">
+          <button type="button" class="btn btn-primary btn-lg px-4 gap-3">{"Sign up"}</button>
+        </div>
+      </div>
+      <div class="col-2 me-auto">
+        <img src="/rih-logo.png" height="400px" />
+      </div>
+    </div>
+  </div>
+  <div class="b-section-divider"></div>
+
+  <div class="container px-5 py-5">
+    <h2 class="pb-2 border-bottom">{"Life in Russia"}</h2>
+    <div class="row row-cols-1 row-cols-md-2 align-items-md-center g-5 py-5">
+      <div class="col d-flex flex-column align-items-start gap-2">
+        <h3 class="fw-bold">{"Moscow is very cool and good indeed"}</h3>
+        <p class="text-body-secondary">{"Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words."}</p>
+        <a href="#sign-up" class="btn btn-primary btn-lg">{"Primary button"}</a>
+      </div>
+      <div class="col">
+        <div class="row row-cols-1 row-cols-sm-2 g-4">
+          <div class="col d-flex flex-column gap-2">
+            <div class="feature-icon-small d-inline-flex align-items-center justify-content-center text-bg-primary bg-gradient fs-4 rounded-3">
+            </div>
+            <h4 class="fw-semibold mb-0">{"Look they have banyas"}</h4>
+            <p class="text-body-secondary">{"Paragraph of text beneath the heading to explain the heading."}</p>
+          </div>
+
+          <div class="col d-flex flex-column gap-2">
+            <div class="feature-icon-small d-inline-flex align-items-center justify-content-center text-bg-primary bg-gradient fs-4 rounded-3">
+            </div>
+            <h4 class="fw-semibold mb-0">{"Wow such cultural diversity"}</h4>
+            <p class="text-body-secondary">{"Paragraph of text beneath the heading to explain the heading."}</p>
+          </div>
+
+          <div class="col d-flex flex-column gap-2">
+            <div class="feature-icon-small d-inline-flex align-items-center justify-content-center text-bg-primary bg-gradient fs-4 rounded-3">
+            </div>
+            <h4 class="fw-semibold mb-0">{"Many nice landscapes indeed"}</h4>
+            <p class="text-body-secondary">{"Paragraph of text beneath the heading to explain the heading."}</p>
+          </div>
+
+          <div class="col d-flex flex-column gap-2">
+            <div class="feature-icon-small d-inline-flex align-items-center justify-content-center text-bg-primary bg-gradient fs-4 rounded-3">
+            </div>
+            <h4 class="fw-semibold mb-0">{"And such low taxes!"}</h4>
+            <p class="text-body-secondary">{"Paragraph of text beneath the heading to explain the heading."}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+  <div class="b-section-divider"></div>
+
+  <div class="container px-4 py-5">
+    <div class="row">
+      <div class="mx-auto col-7">
+        <a id="sign-up"/>
+        <h2 class="pb-2 border-bottom">{"Finding Work in Russia"}</h2>
+        <p>
+          {"Usually landing the most interesting jobs requires you to have a well-developed network of contacts, but this is tough when you set your eyes on a new country. Luckily we at "}
+          <a class="text-black" href={VISTA_URL}>{"Vista Immigration"}</a>
+          {" have contacts with many tech companies in Russia, large and small, and can help you with this!"}</p>
+        <p>{"Tell us a bit about yourself, the technologies you'd like to work with, and your situation in regards to relocating to Russia. We will then match up your profile with companies that match your interests, and establish contact between you and a potential employer if there is a good fit. No generic recruiter spam, guaranteed - we'd rather not send you anything, than send you something irrelevant!"}</p>
+        <p>
+          {"If you get hired, our experts can assist you with legal and other support for your move. Добро пожаловать в Россию!"}
+        </p>
+      </div>
+    </div>
+
+    <div class="row my-3">
+      <div class="col-7 mx-auto">
+        <p>{"Let's get started with a few basic personal details ..."}</p>
+      </div>
+
+      <div class="mx-auto col-6 border rounded-3 shadow">
+        <form class="m-3">
+          <div class="mb-3">
+            <label for="name" class="form-label">{"What's your name?"}</label>
+            <input type="text" class="form-control" id="name"
+                   oninput={link.callback(|event| input_message(event, Msg::SetName))} />
+          </div>
+
+          <div class="mb-3">
+            <label for="email" class="form-label">{"What's your email address?"}</label>
+            <input type="email" class="form-control" id="email" aria-describedby="emailHelp"
+                   oninput={link.callback(|event| input_message(event, Msg::SetEmail))}/>
+            <div id="emailHelp" class="form-text">{"No newsletters, no spam - we will only reach out if there's a match!"}</div>
+          </div>
+
+          <div class="mb-3">
+            <label id="citizenship" class="form-label">{"What citizenship do you hold?"}</label>
+            {citizenship_input(self, link)}
+            <div id="citizenshipHelp" class="form-text">{"We need to know this to estimate immigration-related bureaucracy. If you hold more than one citizenship, pick the one with which you'd want to receive a work visa."}</div>
+          </div>
+
+          <div class="mb-3">
+            <label for="personalDetails" class="form-label">{"Other relevant information:"}</label>
+            <textarea class="form-control" id="personalDetails" rows=3
+                      aria-describedby="personalDetailsHelp"
+                      oninput={link.callback(|event| textarea_message(event, Msg::SetPersonalDetails))} >
+            </textarea>
+            <div id="personalDetailsHelp" class="form-text">{"Any specific places where you'd like to live? Would you be moving with family? Any other assistance required?"}</div>
+          </div>
+
+          <hr />
+          <p>{"Now lets have a look at what you'd like to work with!"}</p>
+
+          <div class="mb-3">
+            <label for="job" class="form-label">{"What job(s) are you looking for?"}</label>
+            <input
+              type="text" class="form-control" id="job"
+              placeholder="Backend/frontend engineer, Test automation, DevOps/SRE, UI/UX ..."
+              oninput={link.callback(|event| input_message(event, Msg::SetPosition))} />
+          </div>
+
+          <div class="mb-3">
+            <label for="technologies" class="form-label">{"Which technologies do you want to work with?"}</label>
+            <div>{render_technologies(link, &self.record.technologies)}</div>
+
+            <input type="text" class="form-control" id="technologies"
+                   aria-describedby="technologiesHelp"
+                   onkeypress={link.callback(add_tech)}/>
+            <div id="technologiesHelp" class="form-text">{"Press enter after each technology."}</div>
+          </div>
+
+          <div class="mb-3">
+            <label for="jobDetails" class="form-label">{"What's your work background?"}</label>
+            <textarea class="form-control" id="workBackground" rows=3
+                      aria-describedby="workBackgroundHelp"
+                      oninput={link.callback(|event| textarea_message(event, Msg::SetWorkBackground))} >
+            </textarea>
+            <div id="workBackgroundHelp" class="form-text">{"Tell us about your work experience, and/or leave links to your CV on your site, LinkedIn or wherever."}</div>
+          </div>
+
+          <div class="mb-3">
+            <label for="jobDetails" class="form-label">{"Other job details:"}</label>
+            <textarea class="form-control" id="jobDetails" rows=3
+                      aria-describedby="jobDetailsHelp"
+                      oninput={link.callback(|event| textarea_message(event, Msg::SetJobDetails))}>
+            </textarea>
+            <div id="jobDetailsHelp" class="form-text">{"Tell us a bit about what you're looking for in a job and in an employer."}</div>
+          </div>
+
+        </form>
+      </div>
+
+    </div>
+  </div>
+  <div class="b-section-divider"></div>
+
+  <footer class="mt-auto text-center">
+    <div class="py-3">
+      <p>
+        {"By "}
+        <a href={VISTA_URL} class="text-black">{"Vista Immigration"}</a>
+        {", with help from "}
+        <a href="https://tvl.su/" class="text-black">{"TVL"}</a>
+        {"."}
+      </p>
+    </div>
+  </footer>
+
+</main>
+}
diff --git a/corp/rih/src/main.rs b/corp/rih/src/main.rs
new file mode 100644
index 000000000000..7360a6979d76
--- /dev/null
+++ b/corp/rih/src/main.rs
@@ -0,0 +1,395 @@
+use fuzzy_matcher::skim::SkimMatcherV2;
+use fuzzy_matcher::FuzzyMatcher;
+use gloo::console;
+use gloo::history::BrowserHistory;
+use gloo::history::History;
+use gloo::storage::{LocalStorage, Storage};
+use rand::seq::IteratorRandom;
+use rand::thread_rng;
+use serde::{Deserialize, Serialize};
+use static_markdown::markdown;
+use std::collections::BTreeSet;
+use wasm_bindgen::closure::Closure;
+use wasm_bindgen::JsCast;
+use web_sys::{HtmlInputElement, HtmlTextAreaElement, KeyboardEvent};
+use yew::html::Scope;
+use yew::prelude::*;
+use yew_router::prelude::*;
+
+/// This code ends up being compiled for the native and for the
+/// webassembly architectures during the build & test process.
+/// However, the `rust_iso3166` crate exposes a different API (!)
+/// based on the platform.
+///
+/// This trait acts as a platform-independent wrapper for the crate.
+///
+/// Upstream issue: https://github.com/rust-iso/rust_iso3166/issues/7
+trait CountryCodeAccess {
+    fn country_alpha2(&self) -> String;
+    fn country_alpha3(&self) -> String;
+    fn country_name(&self) -> String;
+}
+
+#[cfg(target_arch = "wasm32")]
+impl CountryCodeAccess for rust_iso3166::CountryCode {
+    fn country_alpha2(&self) -> String {
+        self.alpha2()
+    }
+
+    fn country_alpha3(&self) -> String {
+        self.alpha3()
+    }
+
+    fn country_name(&self) -> String {
+        self.name()
+    }
+}
+
+#[cfg(not(target_arch = "wasm32"))]
+impl CountryCodeAccess for rust_iso3166::CountryCode {
+    fn country_alpha2(&self) -> String {
+        self.alpha2.to_string()
+    }
+
+    fn country_alpha3(&self) -> String {
+        self.alpha3.to_string()
+    }
+
+    fn country_name(&self) -> String {
+        self.name.to_string()
+    }
+}
+
+const VISTA_URL: &'static str = "https://vista-immigration.ru/";
+
+#[derive(Debug, Clone, Copy, PartialEq, Routable)]
+enum Route {
+    #[at("/")]
+    Home,
+    #[at("/privacy-policy")]
+    PrivacyPolicy,
+    #[not_found]
+    #[at("/404")]
+    NotFound,
+}
+
+/// Represents a single record as filled in by a user. This is the
+/// primary data structure we want to populate and persist somewhere.
+#[derive(Default, Debug, Deserialize, Serialize)]
+struct Record {
+    // Personal information
+    name: String,
+    email: String,
+    citizenship: String, // TODO
+    personal_details: String,
+
+    // Job information
+    position: String,
+    technologies: BTreeSet<String>,
+    job_details: String,
+    work_background: String,
+}
+
+#[derive(Default)]
+struct App {
+    // The record being populated.
+    record: Record,
+
+    // Is the citizenship input focused?
+    citizenship_focus: bool,
+
+    // Current query in the citizenship field.
+    citizenship_query: String,
+
+    // History handler.
+    history: BrowserHistory,
+}
+
+#[derive(Clone, Debug)]
+enum Msg {
+    NoOp,
+    AddTechnology(String),
+    RemoveTechnology(String),
+
+    FocusCitizenship,
+    BlurCitizenship,
+    QueryCitizenship(String),
+    SetCitizenship(String),
+
+    SetName(String),
+    SetEmail(String),
+    SetPersonalDetails(String),
+    SetPosition(String),
+    SetJobDetails(String),
+    SetWorkBackground(String),
+}
+
+/// Callback handler for adding a technology.
+fn add_tech(e: KeyboardEvent) -> Msg {
+    if e.key_code() != 13 {
+        return Msg::NoOp;
+    }
+
+    let input = e.target_unchecked_into::<HtmlInputElement>();
+    let tech = input.value();
+    input.set_value("");
+    Msg::AddTechnology(tech)
+}
+
+fn select_country_enter(event: KeyboardEvent) -> Msg {
+    if event.key_code() != 13 {
+        return Msg::NoOp;
+    }
+
+    let input = event.target_unchecked_into::<HtmlInputElement>();
+    if let Some(country) = fuzzy_country_matches(&input.value()).next() {
+        input.set_value(&country.country_name());
+        return Msg::SetCitizenship(country.country_name());
+    }
+
+    Msg::NoOp
+}
+
+fn fuzzy_country_matches(query: &str) -> Box<dyn Iterator<Item = rust_iso3166::CountryCode> + '_> {
+    if query.is_empty() {
+        let rng = &mut thread_rng();
+        return Box::new(
+            rust_iso3166::ALL
+                .iter()
+                .choose_multiple(rng, 5)
+                .into_iter()
+                .map(Clone::clone),
+        );
+    }
+
+    let matcher = SkimMatcherV2::default();
+    let query = query.to_lowercase();
+    let query_len = query.len();
+
+    let mut results: Vec<_> = rust_iso3166::ALL
+        .iter()
+        .filter_map(|code| {
+            let mut score = None;
+            // Prioritize exact matches for country codes if query length <= 3
+            if query_len <= 3 {
+                if code.country_alpha2().eq_ignore_ascii_case(&query)
+                    || code.country_alpha3().eq_ignore_ascii_case(&query)
+                {
+                    score = Some(100);
+                }
+            }
+            // If no exact match, do a fuzzy match
+            if score.is_none() {
+                score = matcher.fuzzy_match(&code.country_name().to_lowercase(), &query);
+            }
+
+            score.map(|score| (score, code))
+        })
+        .collect();
+
+    // Sort by score in descending order
+    results.sort_by(|a, b| b.0.cmp(&a.0));
+
+    // Get iterator over the best matches
+    Box::new(results.into_iter().map(|(_score, code)| *code))
+}
+
+// Callback for an input element's value being mapped straight into a
+// message.
+fn input_message(e: InputEvent, msg: fn(String) -> Msg) -> Msg {
+    let input = e.target_unchecked_into::<HtmlInputElement>();
+    msg(input.value())
+}
+
+// Callback for a text area's value being mapped straight into a
+// message.
+fn textarea_message(e: InputEvent, msg: fn(String) -> Msg) -> Msg {
+    let textarea = e.target_unchecked_into::<HtmlTextAreaElement>();
+    msg(textarea.value())
+}
+
+fn schedule_blur(event: FocusEvent, link: Scope<App>) -> Msg {
+    let input = event.target_unchecked_into::<HtmlInputElement>();
+    let closure = Closure::once_into_js(Box::new(move || {
+        if let Some(app) = link.get_component() {
+            input.set_value(&app.record.citizenship);
+        }
+
+        link.send_message(Msg::BlurCitizenship);
+    }) as Box<dyn FnOnce()>);
+
+    let window = web_sys::window().expect("no global `window` exists");
+    let _ =
+        window.set_timeout_with_callback_and_timeout_and_arguments_0(closure.unchecked_ref(), 100);
+
+    Msg::NoOp
+}
+
+/// Creates an input field for citizenship selection with suggestions.
+fn citizenship_input(app: &App, link: &Scope<App>) -> Html {
+    let dropdown_classes = if app.citizenship_focus {
+        "dropdown-menu show"
+    } else {
+        "dropdown-menu"
+    };
+
+    let choices = fuzzy_country_matches(&app.citizenship_query).map(|country| {
+        let msg = Msg::SetCitizenship(country.country_name());
+        html! {
+            <li><a class="dropdown-item" onclick={link.callback(move |_| msg.clone())}>{country.country_name()}</a></li>
+        }
+    });
+
+    let blur_link = link.clone();
+    html! {
+      <div class="dropdown">
+        <input type="text" class="form-control" id="citizenship" aria-describedby="citizenshipHelp"
+            autocomplete="off"
+            oninput={link.callback(|event| input_message(event, Msg::QueryCitizenship))}
+            onkeypress={link.callback(select_country_enter)}
+            onfocus={link.callback(|_| Msg::FocusCitizenship)}
+            onblur={link.callback(move |event| schedule_blur(event, blur_link.clone()))} />
+            <ul class={dropdown_classes} style="position: absolute; inset: 0px auto auto 0px; margin: 0px; transform: translate(0px, 40px);" data-popper-placement="bottom-start">
+          { choices.collect::<Html>() }
+        </ul>
+      </div>
+    }
+}
+
+/// Creates a list of technologies which can be deleted again by clicking them.
+fn render_technologies(link: &Scope<App>, technologies: &BTreeSet<String>) -> Html {
+    if technologies.is_empty() {
+        return html! {};
+    }
+
+    let items = technologies.iter().map(|tech| {
+        let msg: Msg = Msg::RemoveTechnology(tech.to_string());
+        html! {
+            <>
+              <span class="btn btn-secondary btn-sm"
+                onclick={link.callback(move |_| msg.clone())}>
+                {tech}
+                <span class="mx-auto text-center text-black">{" x"}</span>
+              </span>{" "}
+            </>
+        }
+    });
+    html! {
+        <div class="p-1">
+            { items.collect::<Html>() }
+        </div>
+    }
+}
+
+impl Component for App {
+    type Message = Msg;
+    type Properties = ();
+
+    fn create(_ctx: &Context<Self>) -> Self {
+        let mut new = Self::default();
+
+        if let Ok(record) = LocalStorage::get("record") {
+            new.record = record;
+        }
+
+        new
+    }
+
+    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+        console::log!("handling ", format!("{:?}", msg));
+        let (state_change, view_change) = match msg {
+            Msg::NoOp => (false, false),
+
+            Msg::AddTechnology(tech) => {
+                console::log!("adding technology", &tech);
+                self.record.technologies.insert(tech);
+                (true, true)
+            }
+
+            Msg::RemoveTechnology(tech) => {
+                console::log!("removing technology ", &tech);
+                self.record.technologies.remove(&tech);
+                (true, true)
+            }
+
+            Msg::QueryCitizenship(query) => {
+                self.citizenship_query = query;
+                (false, true)
+            }
+
+            Msg::FocusCitizenship => {
+                self.citizenship_focus = true;
+                (false, true)
+            }
+
+            Msg::BlurCitizenship => {
+                self.citizenship_focus = false;
+                (false, true)
+            }
+
+            Msg::SetCitizenship(country) => {
+                self.record.citizenship = country;
+                (true, false)
+            }
+
+            Msg::SetName(name) => {
+                self.record.name = name;
+                (true, false)
+            }
+
+            Msg::SetEmail(email) => {
+                self.record.email = email;
+                (true, false)
+            }
+
+            Msg::SetPersonalDetails(details) => {
+                self.record.personal_details = details;
+                (true, false)
+            }
+
+            Msg::SetPosition(position) => {
+                self.record.position = position;
+                (true, false)
+            }
+
+            Msg::SetJobDetails(details) => {
+                self.record.job_details = details;
+                (true, false)
+            }
+
+            Msg::SetWorkBackground(background) => {
+                self.record.work_background = background;
+                (true, false)
+            }
+        };
+
+        if state_change {
+            if let Err(err) = LocalStorage::set("record", &self.record) {
+                console::warn!(
+                    "failed to persist record in local storage: ",
+                    err.to_string()
+                );
+            }
+        }
+
+        view_change
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let link = ctx.link();
+        let location = self.history.location();
+        let route = Route::recognize(location.path()).unwrap_or(Route::NotFound);
+
+        match route {
+            Route::Home => include!("home.html"),
+            Route::PrivacyPolicy => html! {
+                <main>{include!("privacy-policy.md")}</main>
+            },
+            Route::NotFound => todo!(),
+        }
+    }
+}
+
+fn main() {
+    yew::Renderer::<App>::new().render();
+}
diff --git a/corp/rih/src/privacy-policy.md b/corp/rih/src/privacy-policy.md
new file mode 100644
index 000000000000..843aac4c0a90
--- /dev/null
+++ b/corp/rih/src/privacy-policy.md
@@ -0,0 +1,100 @@
+markdown!(r#"
+Privacy Policy
+==============
+
+Effective as of: 2023-05-26
+
+This policy outlines who, how and what happens with personal
+information entered on russiaishiring.com.
+
+0. Parties
+
+    Entities involved in the collection, storage and processing of
+    data are the following, all registered in Russia:
+
+   - [VFBS LLC](https://vista-immigration.ru/) (Vista Foreign Business
+     Support), INN 7709963942, г. Москва, муниципальный округ
+     Таганский вн.тер.г., ул. Таганская, д. 17-23
+
+     Contact: tbis@vfbs.ru
+
+   - [TVL LLC](https://tvl.su/), INN 9703038861, г. Москва, вн.тер.г.
+     муниципальный округ Беговой, ул Правды, д. 24, стр. 2, помещ. 3П
+
+     Contact: contact@tvl.su
+
+   - [Yandex LLC](https://yandex.com/company/), INN 1027700229193,
+     119021, город Москва, ул. Льва Толстого, д.16
+
+     Contacts: https://yandex.com/company/contacts/moscow/
+
+   In the following text, VFBS LLC will be referred to as "we".
+
+1. Information we collect
+
+   We collect information used for the purpose of matching a potential
+   job candidate with a vacancy. This includes:
+
+   - Name
+   - Contact e-mail address
+   - Work background
+   - Citizenship
+   - Language skills
+
+   We collect other **optional** information, such as data about the
+   work background or expectations from a new job, as deemed relevant
+   and supplied by the user.
+
+   As part of our technical processes we also store the IP address
+   from which data was submitted.
+
+2. Data storage
+
+   We store all collected data in services in Yandex Cloud, with the
+   help of our technical services provider TVL LLC.
+
+   Stored data is subject to the [Yandex privacy
+   policy](https://yandex.ru/legal/confidential/).
+
+   Collected data is technically accessible by employees of TVL LLC
+   and VFBC LLC.
+
+3. Data processing
+
+   Data is processed by automatic systems in Yandex Cloud. During
+   processing, no access is granted to any parties not mentioned
+   above.
+
+4. Data sharing with third-parties
+
+   As part of the operation of the service, data may be passed on to
+   companies looking to hire candidates based on the data set.
+
+   This is never done without the user's consent. If you submit data
+   to us and a suitable job opportunity is found, we will reach out to
+   you and confirm the further process as well as any data sharing.
+
+5. User rights
+
+   Users that have submitted data to the service always have the right
+   to request its removal. Please contact us at
+   privacy@russiaishiring.com for help with data removal.
+
+   Note that we may require you to verify your identity through the
+   e-mail address you supplied to us when submitting data.
+
+6. Tracking technologies, cookies, advertising
+
+   We do not have any tracking technologies or advertising on
+   russiaishiring.com. We do not use cookies. Only data explicitly
+   entered by the user in the form is submitted to us.
+
+   The site uses local storage on the user's machine to store form
+   data between visits to the site.
+
+7. Retention period
+
+   We will retain individual data no longer than 2 years from the date
+   of entering the data. If updated data is received for a user, this
+   period is reset.
+"#)
diff --git a/corp/rih/static-markdown/Cargo.toml b/corp/rih/static-markdown/Cargo.toml
new file mode 100644
index 000000000000..c0e298d44cf2
--- /dev/null
+++ b/corp/rih/static-markdown/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+version = "1.0.0"
+name = "static_markdown"
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+syn = "1.0"
+quote = "1.0"
+comrak = "0.18"
diff --git a/corp/rih/static-markdown/src/lib.rs b/corp/rih/static-markdown/src/lib.rs
new file mode 100644
index 000000000000..6a25c9292654
--- /dev/null
+++ b/corp/rih/static-markdown/src/lib.rs
@@ -0,0 +1,27 @@
+extern crate proc_macro;
+
+use comrak::{markdown_to_html, ComrakOptions};
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, LitStr};
+
+#[proc_macro]
+pub fn markdown(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as LitStr);
+
+    let mut options = ComrakOptions::default();
+    options.extension.strikethrough = true;
+    options.extension.tagfilter = true;
+    options.extension.table = true;
+    options.extension.autolink = true;
+
+    let rendered_html = markdown_to_html(&input.value(), &options);
+
+    let tokens = quote! {
+        yew::virtual_dom::VNode::VRaw(yew::virtual_dom::VRaw {
+            html: #rendered_html.into(),
+        })
+    };
+
+    tokens.into()
+}
diff --git a/corp/russian/README.md b/corp/russian/README.md
new file mode 100644
index 000000000000..23c3d594c8de
--- /dev/null
+++ b/corp/russian/README.md
@@ -0,0 +1,9 @@
+//corp/russian
+==============
+
+This folder contains TVL corp projects related to the Russian
+language, such as the code powering
+[Предложник](https://predlozhnik.ru).
+
+Unless otherwise specified, all rights to these projects are reserved
+by ООО "ТВЛ".
diff --git a/corp/russian/data-import/.gitignore b/corp/russian/data-import/.gitignore
new file mode 100644
index 000000000000..e918c641ce99
--- /dev/null
+++ b/corp/russian/data-import/.gitignore
@@ -0,0 +1,2 @@
+target/
+all_events.txt
diff --git a/corp/russian/data-import/Cargo.lock b/corp/russian/data-import/Cargo.lock
new file mode 100644
index 000000000000..cd85e058108f
--- /dev/null
+++ b/corp/russian/data-import/Cargo.lock
@@ -0,0 +1,499 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[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.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bstr"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
+dependencies = [
+ "lazy_static",
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "csv"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1"
+dependencies = [
+ "bstr",
+ "csv-core",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "csv-core"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "data-import"
+version = "0.1.0"
+dependencies = [
+ "csv",
+ "env_logger",
+ "log",
+ "rusqlite",
+ "serde",
+ "xml-rs",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashlink"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
+dependencies = [
+ "hashbrown",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
+dependencies = [
+ "hermit-abi",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[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.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.25.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
+dependencies = [
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+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"
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rusqlite"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
+dependencies = [
+ "bitflags",
+ "fallible-iterator",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
+[[package]]
+name = "rustix"
+version = "0.36.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+
+[[package]]
+name = "serde"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[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.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[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.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
diff --git a/corp/russian/data-import/Cargo.toml b/corp/russian/data-import/Cargo.toml
new file mode 100644
index 000000000000..1aae2e830578
--- /dev/null
+++ b/corp/russian/data-import/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "data-import"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+csv = "1.1"
+env_logger = "0.10.0"
+log = "0.4.17"
+rusqlite = "0.28"
+serde = { version = "1.0", features = ["derive"] }
+xml-rs = "0.8"
+
+[profile.release-with-debug]
+inherits = "release"
+debug = true
diff --git a/corp/russian/data-import/default.nix b/corp/russian/data-import/default.nix
new file mode 100644
index 000000000000..6aa8ad6aa3d8
--- /dev/null
+++ b/corp/russian/data-import/default.nix
@@ -0,0 +1,55 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  buildInputs = with pkgs; [
+    sqlite
+    pkg-config
+  ];
+
+  # mirrored input data from OpenCorpora, as of 2023-01-17.
+  #
+  # This data is licensed under CC-BY-SA.
+  openCorporaArchive = pkgs.fetchurl {
+    name = "dict.opcorpora.xml.bz";
+    url = "https://tazj.in/blobs/opencorpora-20230117.xml.bz2";
+    sha256 = "04n5g43fkfc93z6xlwf2qfdrfdfl562pc2ivdb3cbbbsy56gkqg6";
+  };
+
+  openCorpora = pkgs.runCommand "dict.opcorpora.xml" { } ''
+    ${pkgs.bzip2}/bin/bunzip2 -k -c ${openCorporaArchive} > $out
+  '';
+
+  # mirrored input data from OpenRussian, as of 2023-01-17.
+  #
+  # This data is licensed under CC-BY-SA.
+  openRussianArchive = pkgs.fetchzip {
+    name = "openrussian-20230117";
+    url = "https://tazj.in/blobs/openrussian-20230117.tar.xz";
+    sha256 = "06jl7i23cx58a0n2626hb82xlzimixvnxp7lxdw0g664kv9bmw25";
+  };
+
+  # development shell with native deps
+  shell = pkgs.mkShell {
+    inherit buildInputs;
+
+    # make datasets available in the environment
+    OPENCORPORA_DATA = openCorpora;
+    OPENRUSSIAN_DATA = openRussianArchive;
+  };
+
+in
+lib.fix (self: depot.third_party.naersk.buildPackage {
+  src = depot.third_party.gitignoreSource ./.;
+  inherit buildInputs;
+
+  passthru = depot.nix.readTree.drvTargets {
+    inherit shell openCorpora;
+
+    # target that actually builds an entire database
+    database = pkgs.runCommand "tvl-russian-db.sqlite"
+      {
+        OPENCORPORA_DATA = openCorpora;
+        OPENRUSSIAN_DATA = openRussianArchive;
+      } "${self}/bin/data-import --output $out";
+  };
+})
diff --git a/corp/russian/data-import/src/db_setup.rs b/corp/russian/data-import/src/db_setup.rs
new file mode 100644
index 000000000000..c9fb51738651
--- /dev/null
+++ b/corp/russian/data-import/src/db_setup.rs
@@ -0,0 +1,298 @@
+//! This module prepares the database layout.
+//!
+//! The XML import may be in an arbitrary order, so importing data is
+//! a multi-step process where we first set up schemas matching the
+//! data layout, import the data, and then modify the schema to
+//! introduce things like foreign key constraints between tables that
+//! represent relations.
+
+use super::Ensure;
+use crate::oc_parser::*;
+use crate::or_parser;
+use log::{debug, info};
+use rusqlite::Connection;
+
+/// Sets up an initial schema which matches the OpenCorpora data.
+pub fn initial_oc_schema(conn: &Connection) {
+    conn.execute_batch(
+        r#"
+-- table for plain import of grammemes from XML
+CREATE TABLE oc_grammemes (
+    name TEXT PRIMARY KEY,
+    parent TEXT,
+    alias TEXT,
+    description TEXT
+) STRICT;
+
+-- table for plain import of lemmas (*not* their variations!)
+CREATE TABLE oc_lemmas (
+    id INTEGER PRIMARY KEY,
+    lemma TEXT NOT NULL
+) STRICT;
+
+-- table for relationship between grammemes and lemmas
+CREATE TABLE oc_lemma_grammemes (
+    lemma INTEGER,
+    grammeme TEXT NOT NULL,
+    FOREIGN KEY(lemma) REFERENCES oc_lemmas(id)
+) STRICT;
+
+-- table for all words, i.e. including variations of lemmata
+CREATE TABLE oc_words (
+    lemma INTEGER NOT NULL,
+    word TEXT NOT NULL,
+    FOREIGN KEY(lemma) REFERENCES oc_lemmas(id)
+) STRICT;
+
+-- table for relationship between words and grammemes
+CREATE TABLE oc_word_grammemes (
+    word INTEGER NOT NULL,
+    grammeme TEXT NOT NULL,
+    FOREIGN KEY(word) REFERENCES oc_words(ROWID)
+) STRICT;
+
+-- table for link types
+CREATE TABLE oc_link_types (
+  id INTEGER PRIMARY KEY,
+  name TEXT
+) STRICT;
+
+-- table for links between lemmata
+CREATE TABLE oc_links (
+  id INTEGER PRIMARY KEY,
+  link_type INTEGER NOT NULL,
+  from_lemma INTEGER NOT NULL,
+  to_lemma INTEGER NOT NULL,
+  FOREIGN KEY(link_type) REFERENCES oc_link_types(id),
+  FOREIGN KEY(from_lemma) REFERENCES oc_lemmas(id),
+  FOREIGN KEY(to_lemma) REFERENCES oc_lemmas(id)
+) STRICT;
+
+"#,
+    )
+    .ensure("setting up OpenCorpora table schema failed");
+
+    info!("set up initial table schema for OpenCorpora import");
+}
+
+/// Inserts a single OpenCorpora element into the initial table structure.
+pub fn insert_oc_element(conn: &Connection, elem: OcElement) {
+    match elem {
+        OcElement::Grammeme(grammeme) => {
+            conn.execute(
+                "INSERT INTO oc_grammemes (name, parent, alias, description) VALUES (?1, ?2, ?3, ?4)",
+                (
+                    &grammeme.name,
+                    &grammeme.parent,
+                    &grammeme.alias,
+                    &grammeme.description,
+                ),
+            )
+            .ensure("failed to insert grammeme");
+
+            debug!("inserted grammeme {}", grammeme.name);
+        }
+
+        OcElement::Lemma(lemma) => insert_lemma(conn, lemma),
+
+        OcElement::LinkType(lt) => {
+            conn.execute(
+                "INSERT INTO oc_link_types (id, name) VALUES (?1, ?2)",
+                (&lt.id, &lt.name),
+            )
+            .ensure("failed to insert link type");
+
+            info!("inserted link type {}", lt.name);
+        }
+
+        OcElement::Link(link) => {
+            let mut stmt = conn
+                .prepare_cached(
+                    "INSERT INTO oc_links (id, link_type, from_lemma, to_lemma) VALUES (?1, ?2, ?3, ?4)",
+                )
+                .ensure("failed to prepare link statement");
+
+            stmt.execute((&link.id, &link.link_type, &link.from, &link.to))
+                .ensure("failed to insert link");
+
+            debug!("inserted link {}", link.id);
+        }
+    }
+}
+
+/// Insert a single lemma into the initial structure. This is somewhat
+/// involved because it also establishes a bunch of relations.
+fn insert_lemma(conn: &Connection, lemma: Lemma) {
+    // insert the lemma itself
+    let mut stmt = conn
+        .prepare_cached("INSERT INTO oc_lemmas (id, lemma) VALUES (?1, ?2)")
+        .ensure("failed to prepare statement");
+
+    stmt.execute((&lemma.id, &lemma.lemma.word))
+        .ensure("failed to insert grammeme");
+
+    // followed by its relations to the grammemes set
+    let mut stmt = conn
+        .prepare_cached("INSERT INTO oc_lemma_grammemes (lemma, grammeme) VALUES (?1, ?2)")
+        .ensure("failed to prepare statement");
+
+    for grammeme in lemma.grammemes {
+        stmt.execute((&lemma.id, grammeme))
+            .ensure("failed to insert grammeme<>lemma relationship");
+    }
+
+    // followed by all of its variations ...
+    let mut word_insert = conn
+        .prepare_cached("INSERT INTO oc_words (lemma, word) VALUES (?1, ?2)")
+        .unwrap();
+
+    let mut word_grammeme = conn
+        .prepare_cached("INSERT INTO oc_word_grammemes (word, grammeme) VALUES (?1, ?2)")
+        .unwrap();
+
+    for variation in lemma.variations {
+        // insert the word itself and get its rowid
+        word_insert
+            .execute((&lemma.id, &variation.word))
+            .ensure("failed to insert word");
+        let row_id = conn.last_insert_rowid();
+
+        // then insert its grammeme links
+        for grammeme in variation.grammemes {
+            word_grammeme
+                .execute((row_id, grammeme))
+                .ensure("failed to insert word<>grammeme link");
+        }
+    }
+
+    debug!("inserted lemma {}", lemma.id);
+}
+
+/// Sets up an initial schema for the OpenRussian data.
+pub fn initial_or_schema(conn: &Connection) {
+    conn.execute_batch(
+        r#"
+CREATE TABLE or_words (
+    id INTEGER PRIMARY KEY,
+    bare TEXT NOT NULL,
+    accented TEXT,
+    derived_from_word_id INTEGER,
+    rank INTEGER,
+    word_type TEXT,
+    level TEXT
+) STRICT;
+
+CREATE TABLE or_words_forms (
+    id INTEGER PRIMARY KEY,
+    word_id INTEGER NOT NULL,
+    form_type TEXT,
+    position TEXT,
+    form TEXT,
+    form_bare TEXT,
+    FOREIGN KEY(word_id) REFERENCES words(id)
+) STRICT;
+
+CREATE TABLE or_translations (
+    id INTEGER PRIMARY KEY,
+    word_id INTEGER NOT NULL,
+    translation TEXT,
+    example_ru TEXT,
+    example_tl TEXT,
+    info TEXT,
+    FOREIGN KEY(word_id) REFERENCES words(id)
+) STRICT;
+"#,
+    )
+    .ensure("setting up OpenRussian table schema failed");
+
+    info!("set up initial table schema for OpenRussian import");
+}
+
+pub fn insert_or_words<I: Iterator<Item = or_parser::Word>>(conn: &Connection, words: I) {
+    let mut stmt = conn
+        .prepare_cached(
+            "
+INSERT INTO or_words (id, bare, accented, derived_from_word_id, rank, word_type, level)
+VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
+",
+        )
+        .ensure("failed to prepare OR words statement");
+    let mut count = 0;
+
+    for word in words {
+        stmt.execute((
+            word.id,
+            word.bare,
+            word.accented,
+            word.derived_from_word_id,
+            word.rank,
+            word.word_type,
+            word.level,
+        ))
+        .ensure("failed to insert OR word");
+        count += 1;
+    }
+
+    info!("inserted {} OpenRussian words", count);
+}
+
+pub fn insert_or_word_forms<I: Iterator<Item = or_parser::WordForm>>(conn: &Connection, forms: I) {
+    let mut stmt = conn
+        .prepare_cached(
+            "
+INSERT INTO or_words_forms (id, word_id, form_type, position, form, form_bare)
+VALUES (?1, ?2, ?3, ?4, ?5, ?6)
+",
+        )
+        .ensure("failed to prepare OR word forms statement");
+    let mut count = 0;
+
+    for form in forms {
+        stmt.execute((
+            form.id,
+            form.word_id,
+            form.form_type,
+            form.position,
+            form.form,
+            form.form_bare,
+        ))
+        .ensure("failed to insert OR word form");
+        count += 1;
+    }
+
+    info!("inserted {} OpenRussian word forms", count);
+}
+
+pub fn insert_or_translations<I: Iterator<Item = or_parser::Translation>>(
+    conn: &Connection,
+    translations: I,
+) {
+    let mut stmt = conn
+        .prepare_cached(
+            "INSERT INTO or_translations (id, word_id, translation, example_ru, example_tl, info)
+             VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
+        )
+        .ensure("failed to prepare OR translation statement");
+
+    let mut count = 0;
+
+    for tl in translations {
+        if tl.lang != "en" {
+            continue;
+        }
+
+        stmt.execute((
+            tl.id,
+            tl.word_id,
+            tl.tl,
+            tl.example_ru,
+            tl.example_tl,
+            tl.info,
+        ))
+        .ensure("failed to insert OR translation");
+
+        count += 1;
+    }
+
+    info!("inserted {} OpenRussian translations", count);
+}
diff --git a/corp/russian/data-import/src/main.rs b/corp/russian/data-import/src/main.rs
new file mode 100644
index 000000000000..21da48e8d8f4
--- /dev/null
+++ b/corp/russian/data-import/src/main.rs
@@ -0,0 +1,298 @@
+//! This program imports Russian language data from OpenCorpora
+//! ("Открытый корпус") and OpenRussian into a SQLite database that
+//! can be used for [//corp/russian][corp-russian] projects.
+//!
+//! [corp-russian]: https://at.tvl.fyi/?q=%2F%2Fcorp%2Frussian
+//!
+//! Ideally, running this on intact dumps should yield a fully
+//! functional SQLite database compatible with all other tools
+//! consuming it.
+//!
+//! ## OpenCorpora format
+//!
+//! The format used is partially documented on the [OpenCorpora
+//! website][format-docs]. This seems to be a slightly outdated
+//! format, however, hence some information about what the format
+//! seems to be today.
+//!
+//! [format-docs]: http://opencorpora.org/?page=export
+//!
+//! The format is an XML file, which has several categories of data,
+//! each with their own schema:
+//!
+//! * `grammemes`: These define units of grammar. They're *likely* pretty
+//!   static, and we'll *likely* want to map them into a custom set of
+//!   (simpler) categories.
+//!
+//!   They form some kind of internal hierarchy, where some of them have a
+//!   `parent` attribute set to some other grammemes `name`.
+//!
+//!   There's a ridiculous number of these.
+//!
+//! * `restrictions`: Unclear, not documented on the page. They describe
+//!   something about the relationship between grammemes.
+//!
+//! * `lemmata`: this lists the actual lemmas, as well as all their
+//!   included morphological variants
+//!
+//!   Each lemma has an `id` attribute uniquely identifying its dictionary
+//!   form, as well as a number of sub-elements:
+//!
+//!   * the `l` attribute contains the lemma itself
+//!   * the `f` attributes contain morphological variations
+//!
+//!   Each of these sub elements again contains a number of `g` elements,
+//!   which refer to the IDs of grammems in their `v` attributes.
+//!
+//! * `<link_types>` These list possible "relationships between lemmas",
+//!   basically just assigning them IDs and names. There's only 27 of
+//!   these.
+//!
+//! * `<links>`: Using the types defined above, this establishes links
+//!   between lemmas that have some kind of relationship.
+//!
+//!   For example, a relationship `cardinal/ordinal` might be established
+//!   between the lemmas "два" and "второй".
+//!
+//! ## OpenRussian format
+//!
+//! The [OpenRussian](https://en.openrussian.org/dictionary) project
+//! lets users export its database as a set of CSV-files. For our
+//! purposes, we download the files using `<tab>` separators.
+//!
+//! Whereas OpenCorpora opts for a flat structure with a "tag" system
+//! (through its flexible grammemes), OpenRussian has a fixed pre-hoc
+//! structure into which it sorts some words with their morphologies.
+//! The OpenRussian database is much smaller as of January 2023 (~1.7
+//! million words vs. >5 million for OpenCorpora), but some of the
+//! information is much more practically useful.
+//!
+//! Two very important bits of information OpenRussian has are accent
+//! marks (most tables containing actual words have a normal form
+//! containing and accent mark, and a "bare" form without) and
+//! translations into English and German.
+//!
+//! The full dump includes the following tables (and some more):
+//!
+//! * `words`: List of lemmas in the corpus, with various bits of
+//!    metadata as well as hand-written notes.
+//!
+//! * `adjectives`: Contains IDs for words that are adjectives.
+//!
+//! * `nouns`: IDs for words that are nouns; and noun metadata (e.g.
+//!   gender, declinability)
+//!
+//! * `verbs`: IDs of words that are verbs, including their aspect and
+//!   "partnered" verb in the other aspect
+//!
+//! * `words_forms`: Contains all morphed variants of the lemmas from
+//!   `words`, including information about their grammeme, and accent
+//!   marks.
+//!
+//! * `words_rels`: Contains relations between words, containing
+//!   information like "synonyms" or general relation between words.
+//!
+//! * `translations`: Contains translations tagged by target language,
+//!   as well as examples and (occasionally) additional information.
+//!
+//! These tables also contain something, but have not been analysed
+//! yet:
+//!
+//! * `expressions_words`
+//! * `sentences`
+//! * `sentences_translations`
+//! * `sentences_words`
+
+use log::{error, info};
+use rusqlite::{Connection, Result};
+use std::env;
+use std::fmt::Display;
+use std::fs::File;
+use std::io::BufReader;
+
+mod db_setup;
+mod mappings;
+mod oc_parser;
+mod or_parser;
+
+struct Args {
+    output: String,
+    or_input: String,
+    oc_input: String,
+}
+
+impl Args {
+    fn populated(&self) -> bool {
+        !(self.output.is_empty() || self.or_input.is_empty() || self.oc_input.is_empty())
+    }
+}
+
+fn usage(binary_name: &str) {
+    bail(format!(
+        "usage: {} --output <output-file> --or-input <or-input> --oc-input <oc-input>",
+        binary_name
+    ));
+}
+
+fn parse_args() -> Args {
+    let mut args_iter = env::args();
+    let binary_name = args_iter.next().unwrap();
+
+    let mut args = Args {
+        output: "".into(),
+        or_input: env::var("OPENRUSSIAN_DATA").unwrap_or_default(),
+        oc_input: env::var("OPENCORPORA_DATA").unwrap_or_default(),
+    };
+
+    loop {
+        if args.populated() {
+            break;
+        }
+
+        while let Some(arg) = args_iter.next() {
+            match arg.as_str() {
+                "--output" => {
+                    args.output = args_iter.next().unwrap();
+                }
+
+                "--or-input" => {
+                    args.or_input = args_iter.next().unwrap();
+                }
+
+                "--oc-input" => {
+                    args.oc_input = args_iter.next().unwrap();
+                }
+
+                _ => usage(&binary_name),
+            }
+        }
+    }
+
+    if args.output.is_empty() || args.or_input.is_empty() || args.oc_input.is_empty() {
+        usage(&binary_name);
+    }
+
+    args
+}
+
+fn open_corpora(conn: &Connection, args: &Args) {
+    let input_file = File::open(&args.oc_input).ensure("failed to open input file");
+    let mut parser = oc_parser::OpenCorporaParser::new(BufReader::new(input_file));
+    db_setup::initial_oc_schema(&conn);
+
+    let mut tx = conn
+        .unchecked_transaction()
+        .ensure("failed to start transaction");
+
+    let mut count = 0;
+
+    while let Some(elem) = parser.next_element() {
+        // commit every 1000 things
+        if count % 1000 == 0 {
+            tx.commit().ensure("transaction failed");
+            tx = conn
+                .unchecked_transaction()
+                .ensure("failed to start new transaction");
+            info!("transaction committed at watermark {}", count);
+        }
+
+        db_setup::insert_oc_element(&tx, elem);
+
+        count += 1;
+    }
+
+    tx.commit().ensure("final OpenCorpora commit failed");
+
+    info!("finished OpenCorpora import");
+}
+
+fn open_russian(conn: &Connection, args: &Args) {
+    let parser = or_parser::OpenRussianParser::new(&args.or_input);
+
+    db_setup::initial_or_schema(conn);
+
+    {
+        let tx = conn
+            .unchecked_transaction()
+            .ensure("failed to start transaction");
+
+        db_setup::insert_or_words(&tx, parser.words());
+        tx.commit().ensure("OpenRussian words commit failed");
+    }
+
+    {
+        let tx = conn
+            .unchecked_transaction()
+            .ensure("failed to start transaction");
+
+        db_setup::insert_or_word_forms(&tx, parser.words_forms());
+        tx.commit().ensure("OpenRussian word forms commit failed");
+    }
+
+    {
+        let tx = conn
+            .unchecked_transaction()
+            .ensure("failed to start transaction");
+
+        db_setup::insert_or_translations(&tx, parser.translations());
+        tx.commit().ensure("OpenRussian translations commit failed");
+    }
+
+    info!("finished OpenRussian import");
+}
+
+fn main() {
+    env_logger::builder()
+        .filter_level(log::LevelFilter::Info)
+        .init();
+
+    let args = parse_args();
+
+    info!("output path: {}", args.output);
+    info!("OpenCorpora input path: {}", args.oc_input);
+    info!("OpenRussian input path: {}", args.or_input);
+
+    let conn = Connection::open(&args.output).ensure("failed to open DB connection");
+
+    open_corpora(&conn, &args);
+    open_russian(&conn, &args);
+
+    // afterwards:
+    // add actual IDs to grammemes
+    // properly reference keys internally
+    // add foreign key constraint on lemma_grammemes.grammeme
+}
+
+/// It's like `expect`, but through `log::error`.
+trait Ensure<T> {
+    fn ensure<S: Into<String>>(self, msg: S) -> T;
+}
+
+impl<T, E: Display> Ensure<T> for Result<T, E> {
+    fn ensure<S: Into<String>>(self, msg: S) -> T {
+        match self {
+            Ok(x) => x,
+            Err(err) => {
+                error!("{}: {}", msg.into(), err);
+                std::process::exit(1);
+            }
+        }
+    }
+}
+
+impl<T> Ensure<T> for Option<T> {
+    fn ensure<S: Into<String>>(self, msg: S) -> T {
+        match self {
+            Some(x) => x,
+            None => {
+                error!("{}", msg.into());
+                std::process::exit(1);
+            }
+        }
+    }
+}
+
+fn bail<S: Into<String>>(msg: S) -> ! {
+    error!("{}", msg.into());
+    std::process::exit(1);
+}
diff --git a/corp/russian/data-import/src/mappings.rs b/corp/russian/data-import/src/mappings.rs
new file mode 100644
index 000000000000..985088a56628
--- /dev/null
+++ b/corp/russian/data-import/src/mappings.rs
@@ -0,0 +1,185 @@
+//! Manual mapping of some data structures in OC/OR corpora.
+
+/// Maps the *names* of OpenRussian word types (the `word_type` field
+/// in the `or_words` table) to the *set* of OpenCorpora grammemes
+/// commonly attached to lemmata of this type in OC.
+///
+/// Some word types just don't map over, and are omitted. Many words
+/// also have an empty word type.
+pub const WORD_TYPES_GRAMMEME_MAP: &'static [(&'static str, &'static [&'static str])] = &[
+    ("adjective", &["ADJF"]),
+    ("adverb", &["ADVB"]),
+    ("noun", &["NOUN"]),
+    ("verb", &["INFN"]), // or "VERB" ...
+];
+
+/// Maps the *names* of OpenRussian grammemes (the `form_type` fields
+/// in the `or_word_forms` table) to the *set* of OpenCorpora
+/// grammemes attached to them corresponding lemma in the `oc_lemmas`
+/// table.
+///
+/// This *only* includes grammatical information about the lemma of
+/// the word (such as whether it is a verb or other type), but *not*
+/// information about the specific instance of the word (such as its
+/// gender).
+///
+/// Correctly corresponding these requires use of all mapping tables.
+pub const FORMS_LEMMATA_GRAMMEME_MAP: &'static [(&'static str, &'static [&'static str])] = &[
+    ("ru_adj_comparative", &["COMP"]),
+    ("ru_adj_superlative", &["ADJF", "Supr"]),
+    ("ru_adj_f_acc", &["ADJF"]),
+    ("ru_adj_f_dat", &["ADJF"]),
+    ("ru_adj_f_gen", &["ADJF"]),
+    ("ru_adj_f_inst", &["ADJF"]),
+    ("ru_adj_f_nom", &["ADJF"]),
+    ("ru_adj_f_prep", &["ADJF"]),
+    ("ru_adj_m_acc", &["ADJF"]),
+    ("ru_adj_m_dat", &["ADJF"]),
+    ("ru_adj_m_gen", &["ADJF"]),
+    ("ru_adj_m_inst", &["ADJF"]),
+    ("ru_adj_m_nom", &["ADJF"]),
+    ("ru_adj_m_prep", &["ADJF"]),
+    ("ru_adj_n_acc", &["ADJF"]),
+    ("ru_adj_n_dat", &["ADJF"]),
+    ("ru_adj_n_gen", &["ADJF"]),
+    ("ru_adj_n_inst", &["ADJF"]),
+    ("ru_adj_n_nom", &["ADJF"]),
+    ("ru_adj_n_prep", &["ADJF"]),
+    ("ru_adj_pl_acc", &["ADJF"]),
+    ("ru_adj_pl_dat", &["ADJF"]),
+    ("ru_adj_pl_gen", &["ADJF"]),
+    ("ru_adj_pl_inst", &["ADJF"]),
+    ("ru_adj_pl_nom", &["ADJF"]),
+    ("ru_adj_pl_prep", &["ADJF"]),
+    ("ru_adj_short_f", &["ADJS"]),
+    ("ru_adj_short_m", &["ADJS"]),
+    ("ru_adj_short_n", &["ADJS"]),
+    ("ru_adj_short_pl", &["ADJS"]),
+    ("ru_noun_pl_acc", &["NOUN"]),
+    ("ru_noun_pl_dat", &["NOUN"]),
+    ("ru_noun_pl_gen", &["NOUN"]),
+    ("ru_noun_pl_inst", &["NOUN"]),
+    ("ru_noun_pl_nom", &["NOUN"]),
+    ("ru_noun_pl_prep", &["NOUN"]),
+    ("ru_noun_sg_acc", &["NOUN"]),
+    ("ru_noun_sg_dat", &["NOUN"]),
+    ("ru_noun_sg_gen", &["NOUN"]),
+    ("ru_noun_sg_inst", &["NOUN"]),
+    ("ru_noun_sg_nom", &["NOUN"]),
+    ("ru_noun_sg_prep", &["NOUN"]),
+    ("ru_verb_gerund_past", &["GRND"]),
+    ("ru_verb_gerund_present", &["GRND"]),
+    ("ru_verb_imperative_pl", &["VERB"]),
+    ("ru_verb_imperative_sg", &["VERB"]),
+    ("ru_verb_past_f", &["VERB"]),
+    ("ru_verb_past_m", &["VERB"]),
+    ("ru_verb_past_n", &["VERB"]),
+    ("ru_verb_past_pl", &["VERB"]),
+    ("ru_verb_presfut_pl1", &["VERB"]),
+    ("ru_verb_presfut_pl2", &["VERB"]),
+    ("ru_verb_presfut_pl3", &["VERB"]),
+    ("ru_verb_presfut_sg1", &["VERB"]),
+    ("ru_verb_presfut_sg2", &["VERB"]),
+    ("ru_verb_presfut_sg3", &["VERB"]),
+    (
+        "ru_base",
+        &[ /* nothing consistent, except often 'Fixd' */ ],
+    ),
+    ("ru_verb_participle_active_past", &["PRTF", "past", "actv"]),
+    (
+        "ru_verb_participle_active_present",
+        &["PRTF", "pres", "actv"],
+    ),
+    (
+        "ru_verb_participle_passive_past",
+        &["PRTF", "past", "passv"],
+    ),
+    (
+        "ru_verb_participle_passive_present",
+        &["PRTF", "pres", "passv"],
+    ),
+];
+
+/// Maps the *names* of OpenRussian grammemes (the `form_type` fields
+/// in the `or_word_forms` table) to the *set* of OpenCorpora
+/// grammemes attached to them corresponding words in the `oc_words`
+/// table.
+///
+/// This includes grammatical information about the "instance" of the
+/// word (such as its gender), but *not* the higher-level type
+/// information about its lemma.
+///
+/// Correctly corresponding these requires use of all mapping tables.
+pub const FORMS_WORDS_GRAMMEME_MAP: &'static [(&'static str, &'static [&'static str])] = &[
+    ("ru_adj_comparative", &["Cmp2"]),
+    ("ru_adj_f_acc", &["femn", "sing", "accs"]),
+    ("ru_adj_f_dat", &["femn", "sing", "datv"]),
+    ("ru_adj_f_gen", &["femn", "sing", "gent"]),
+    ("ru_adj_f_inst", &["femn", "sing", "ablt"]),
+    ("ru_adj_f_nom", &["femn", "sing", "nomn"]),
+    ("ru_adj_f_prep", &["femn", "sing", "loct"]),
+    ("ru_adj_m_acc", &["masc", "sing", "accs"]),
+    ("ru_adj_m_dat", &["masc", "sing", "datv"]),
+    ("ru_adj_m_gen", &["masc", "sing", "gent"]),
+    ("ru_adj_m_inst", &["masc", "sing", "ablt"]),
+    ("ru_adj_m_nom", &["masc", "sing", "nomn"]),
+    ("ru_adj_m_prep", &["masc", "sing", "loct"]),
+    ("ru_adj_n_acc", &["neut", "sing", "accs"]),
+    ("ru_adj_n_dat", &["neut", "sing", "datv"]),
+    ("ru_adj_n_gen", &["neut", "sing", "gent"]),
+    ("ru_adj_n_inst", &["neut", "sing", "ablt"]),
+    ("ru_adj_n_nom", &["neut", "sing", "nomn"]),
+    ("ru_adj_n_prep", &["neut", "sing", "loct"]),
+    ("ru_adj_pl_acc", &["plur", "accs"]),
+    ("ru_adj_pl_dat", &["plur", "datv"]),
+    ("ru_adj_pl_gen", &["plur", "gent"]),
+    ("ru_adj_pl_inst", &["plur", "ablt"]),
+    ("ru_adj_pl_nom", &["plur", "nomn"]),
+    ("ru_adj_pl_prep", &["plur", "loct"]),
+    ("ru_adj_short_f", &["femn", "sing"]),
+    ("ru_adj_short_m", &["masc", "sing"]),
+    ("ru_adj_short_n", &["neut", "sing"]),
+    ("ru_adj_short_pl", &["plur"]),
+    ("ru_noun_pl_acc", &["plur", "accs"]),
+    ("ru_noun_pl_dat", &["plur", "datv"]),
+    ("ru_noun_pl_gen", &["plur", "gent"]),
+    ("ru_noun_pl_inst", &["plur", "ablt"]),
+    ("ru_noun_pl_nom", &["plur", "nomn"]),
+    ("ru_noun_pl_prep", &["plur", "loct"]),
+    ("ru_noun_sg_acc", &["sing", "accs"]),
+    ("ru_noun_sg_dat", &["sing", "datv"]),
+    ("ru_noun_sg_gen", &["sing", "gent"]),
+    ("ru_noun_sg_inst", &["sing", "ablt"]),
+    ("ru_noun_sg_nom", &["sing", "nomn"]),
+    ("ru_noun_sg_prep", &["sing", "loct"]),
+    ("ru_verb_gerund_past", &["past", "V-sh"]),
+    ("ru_verb_imperative_pl", &["plur", "impr"]),
+    ("ru_verb_imperative_sg", &["sing", "impr"]),
+    ("ru_verb_past_f", &["femn", "sing", "past"]),
+    ("ru_verb_past_m", &["masc", "sing", "past"]),
+    ("ru_verb_past_n", &["neut", "sing", "past"]),
+    ("ru_verb_past_pl", &["plur", "past"]),
+    // these also contain "pres" or "futr", depending on the verb.
+    ("ru_verb_presfut_pl1", &["plur", "1per"]),
+    ("ru_verb_presfut_pl2", &["plur", "2per"]),
+    ("ru_verb_presfut_pl3", &["plur", "3per"]),
+    ("ru_verb_presfut_sg1", &["sing", "1per"]),
+    ("ru_verb_presfut_sg2", &["sing", "2per"]),
+    ("ru_verb_presfut_sg3", &["sing", "3per"]),
+    // Unclear items, probably only useful tags on lemmata
+    (
+        "ru_verb_gerund_present",
+        &["pres" /* prob. something missing? */],
+    ),
+    (
+        "ru_adj_superlative",
+        &[/* TODO: unclear, random list of grammemes?! */],
+    ),
+    ("ru_base", &[/* TODO: unclear */]),
+    // These have no useful tags in the forms table, only gender &
+    // case tagging.
+    ("ru_verb_participle_active_past", &[]),
+    ("ru_verb_participle_active_present", &[]),
+    ("ru_verb_participle_passive_past", &[]),
+    ("ru_verb_participle_passive_present", &[]),
+];
diff --git a/corp/russian/data-import/src/oc_parser.rs b/corp/russian/data-import/src/oc_parser.rs
new file mode 100644
index 000000000000..8103ebd92369
--- /dev/null
+++ b/corp/russian/data-import/src/oc_parser.rs
@@ -0,0 +1,470 @@
+use super::{bail, Ensure};
+use log::{info, warn};
+use std::str::FromStr;
+use xml::attribute::OwnedAttribute;
+use xml::name::OwnedName;
+use xml::reader::XmlEvent;
+use xml::EventReader;
+
+#[derive(Default, Debug)]
+pub struct Grammeme {
+    pub parent: Option<String>,
+    pub name: String,
+    pub alias: String,
+    pub description: String,
+}
+
+/// Single form of a word (either its lemma, or the variations).
+#[derive(Debug, Default)]
+pub struct Variation {
+    pub word: String,
+    pub grammemes: Vec<String>,
+}
+
+#[derive(Debug, Default)]
+pub struct Lemma {
+    pub id: u64,
+    pub lemma: Variation,
+    pub grammemes: Vec<String>,
+    pub variations: Vec<Variation>,
+}
+
+#[derive(Debug, Default)]
+pub struct LinkType {
+    pub id: u64,
+    pub name: String,
+}
+
+#[derive(Debug, Default)]
+pub struct Link {
+    pub id: u64,   // link itself
+    pub from: u64, // lemma
+    pub to: u64,   // lemma
+    pub link_type: u64,
+}
+
+#[derive(Debug)]
+pub enum OcElement {
+    Grammeme(Grammeme),
+    Lemma(Lemma),
+    LinkType(LinkType),
+    Link(Link),
+}
+
+#[derive(Debug, PartialEq)]
+enum ParserState {
+    /// Parser is not parsing any particular section and waiting for a
+    /// start tag instead.
+    Init,
+
+    /// Parser is parsing grammemes.
+    Grammemes,
+
+    /// Parser is parsing lemmata.
+    Lemmata,
+
+    /// Parser is inside a lemma's actual lemma.
+    Lemma,
+
+    /// Parser is parsing a morphological variation of a lemma.
+    Variation,
+
+    /// Parser is parsing link types.
+    LinkTypes,
+
+    /// Parser is parsing links.
+    Links,
+
+    /// Parser has seen the end of the line and nothing more is
+    /// available.
+    Ended,
+}
+
+pub struct OpenCorporaParser<R: std::io::Read> {
+    reader: EventReader<R>,
+    state: ParserState,
+}
+
+#[derive(PartialEq)]
+enum SectionState {
+    /// Actively interested in parsing this section.
+    Active,
+
+    /// Section is known, but currently ignored.
+    Inactive,
+
+    /// Section is unknown (probably a bug).
+    Unknown,
+}
+
+fn section_state(section: &str) -> SectionState {
+    match section {
+        "grammemes" | "lemmata" | "link_types" | "links" => SectionState::Active,
+        "restrictions" => SectionState::Inactive,
+        _ => SectionState::Unknown,
+    }
+}
+
+impl<R: std::io::Read> OpenCorporaParser<R> {
+    pub fn new(reader: R) -> Self {
+        let config = xml::ParserConfig::new().trim_whitespace(true);
+        let reader = EventReader::new_with_config(reader, config);
+
+        Self {
+            reader,
+            state: ParserState::Init,
+        }
+    }
+
+    /// Pull an `OcElement` out of the parser. Returns `None` if the
+    /// parser stream has ended.
+    pub fn next_element(&mut self) -> Option<OcElement> {
+        if self.state == ParserState::Ended {
+            return None;
+        }
+
+        // Pull the next element to determine what context to enter
+        // next.
+        loop {
+            match &self.next() {
+                // no-op events that do not affect parser state
+                XmlEvent::Comment(_)
+                | XmlEvent::Whitespace(_)
+                | XmlEvent::ProcessingInstruction { .. }
+                | XmlEvent::StartDocument { .. } => continue,
+                XmlEvent::StartElement { name, .. } | XmlEvent::EndElement { name }
+                    if name.local_name == "dictionary" =>
+                {
+                    continue
+                }
+
+                // end of the file, nothing more to return
+                XmlEvent::EndDocument => {
+                    self.state = ParserState::Ended;
+                    return None;
+                }
+
+                // some sections are skipped
+                XmlEvent::StartElement { name, .. } | XmlEvent::EndElement { name }
+                    if section_state(&name.local_name) == SectionState::Inactive =>
+                {
+                    info!("skipping {} section", name.local_name);
+                    self.skip_section(&name.local_name);
+                }
+
+                // active section events start specific parser states ...
+                XmlEvent::StartElement { name, .. }
+                    if section_state(&name.local_name) == SectionState::Active =>
+                {
+                    self.state = match name.local_name.as_str() {
+                        "grammemes" => ParserState::Grammemes,
+                        "lemmata" => ParserState::Lemmata,
+                        "link_types" => ParserState::LinkTypes,
+                        "links" => ParserState::Links,
+                        _ => unreachable!(),
+                    };
+                }
+
+                // ... or end them
+                XmlEvent::EndElement { name, .. }
+                    if section_state(&name.local_name) == SectionState::Active =>
+                {
+                    // TODO: assert that the right section ended
+                    self.state = ParserState::Init;
+                }
+
+                // actual beginning of an actual element, dispatch accordingly
+                event @ XmlEvent::StartElement {
+                    name, attributes, ..
+                } => match &self.state {
+                    ParserState::Grammemes => {
+                        return Some(OcElement::Grammeme(self.parse_grammeme(name, attributes)))
+                    }
+
+                    ParserState::Lemmata => {
+                        return Some(OcElement::Lemma(self.parse_lemma(name, attributes)))
+                    }
+
+                    ParserState::LinkTypes => {
+                        return Some(OcElement::LinkType(self.parse_link_type(name, attributes)))
+                    }
+
+                    ParserState::Links if name.local_name == "link" => {
+                        return Some(OcElement::Link(self.parse_link(attributes)))
+                    }
+
+                    ParserState::Init | ParserState::Ended => bail(format!(
+                        "parser received an unexpected start element while in state {:?}: {:?}",
+                        self.state, event
+                    )),
+
+                    other => bail(format!(
+                        "next_element() called while parser was in state {:?}",
+                        other
+                    )),
+                },
+
+                // finally, events that indicate a bug if they're
+                // encountered here
+                event @ XmlEvent::EndElement { .. }
+                | event @ XmlEvent::CData(_)
+                | event @ XmlEvent::Characters(_) => {
+                    bail(format!("unexpected XML event: {:?}", event))
+                }
+            }
+        }
+    }
+
+    /// Skip a section by advancing the parser state until we see an
+    /// end element for the skipped section.
+    fn skip_section(&mut self, section: &str) {
+        loop {
+            match self.next() {
+                XmlEvent::EndElement { name } if name.local_name == section => return,
+                _ => continue,
+            }
+        }
+    }
+
+    fn next(&mut self) -> XmlEvent {
+        self.reader.next().ensure("XML parsing failed")
+    }
+
+    /// Parse a tag that should have plain string content.
+    fn parse_string(&mut self, tag_name: &str) -> String {
+        let mut out = String::new();
+
+        loop {
+            match self.next() {
+                // ignore irrelevant things
+                XmlEvent::Comment(_) | XmlEvent::Whitespace(_) => continue,
+
+                // set the content
+                XmlEvent::Characters(content) => {
+                    out = content;
+                }
+
+                // expect the end of the element
+                XmlEvent::EndElement { name } if name.local_name == tag_name => return out,
+
+                // fail on everything unexpected
+                event => bail(format!(
+                    "unexpected element while parsing <{}>: {:?}",
+                    tag_name, event
+                )),
+            }
+        }
+    }
+
+    /// Parse a single `<grammeme>` tag.
+    fn parse_grammeme(&mut self, name: &OwnedName, attributes: &[OwnedAttribute]) -> Grammeme {
+        if name.local_name != "grammeme" {
+            bail(format!(
+                "expected to parse a grammeme, but found <{}>",
+                name.local_name
+            ));
+        }
+
+        let mut grammeme = Grammeme::default();
+
+        for attr in attributes {
+            if attr.name.local_name == "parent" && !attr.value.is_empty() {
+                grammeme.parent = Some(attr.value.clone());
+            }
+        }
+
+        loop {
+            match self.next() {
+                // ignore irrelevant things
+                XmlEvent::Comment(_) | XmlEvent::Whitespace(_) => continue,
+
+                // expect known tags
+                XmlEvent::StartElement { name, .. } if name.local_name == "name" => {
+                    grammeme.name = self.parse_string("name");
+                }
+
+                XmlEvent::StartElement { name, .. } if name.local_name == "alias" => {
+                    grammeme.alias = self.parse_string("alias");
+                }
+
+                XmlEvent::StartElement { name, .. } if name.local_name == "description" => {
+                    grammeme.description = self.parse_string("description");
+                }
+
+                // handle end of the grammeme
+                XmlEvent::EndElement { name } if name.local_name == "grammeme" => break,
+
+                // fail on everything unexpected
+                event => bail(format!(
+                    "unexpected element while parsing <grammeme>: {:?}",
+                    event
+                )),
+            }
+        }
+
+        grammeme
+    }
+
+    fn parse_lemma(&mut self, name: &OwnedName, attributes: &[OwnedAttribute]) -> Lemma {
+        if name.local_name != "lemma" {
+            bail(format!(
+                "expected to parse a lemma, but found <{}>",
+                name.local_name
+            ));
+        }
+
+        self.state = ParserState::Lemma;
+        let mut lemma = Lemma::default();
+
+        for attr in attributes {
+            if attr.name.local_name == "id" {
+                lemma.id = u64::from_str(&attr.value).ensure("failed to parse lemma ID");
+            }
+        }
+
+        loop {
+            match self.next() {
+                // <lemma> has ended
+                XmlEvent::EndElement { name } if name.local_name == "lemma" => {
+                    self.state = ParserState::Lemmata;
+                    return lemma;
+                }
+
+                // actual lemma content
+                XmlEvent::StartElement {
+                    name, attributes, ..
+                } => {
+                    match name.local_name.as_str() {
+                        // beginning to parse the lemma itself
+                        "l" => {
+                            lemma.lemma.word = attributes
+                                .into_iter()
+                                .find(|attr| attr.name.local_name == "t")
+                                .map(|attr| attr.value)
+                                .ensure(format!("lemma {} had no actual word", lemma.id));
+                        }
+
+                        // parsing a lemma variation
+                        "f" => {
+                            self.state = ParserState::Variation;
+
+                            let word = attributes
+                                .into_iter()
+                                .find(|attr| attr.name.local_name == "t")
+                                .map(|attr| attr.value)
+                                .ensure(format!(
+                                    "variation of lemma {} had no actual word",
+                                    lemma.id
+                                ));
+
+                            lemma.variations.push(Variation {
+                                word,
+                                grammemes: vec![],
+                            });
+                        }
+
+                        // parse a grammeme association
+                        "g" => {
+                            let grammeme = attributes
+                                .into_iter()
+                                .find(|attr| attr.name.local_name == "v")
+                                .map(|attr| attr.value)
+                                .ensure(format!(
+                                    "grammeme association in lemma {} missing ID",
+                                    lemma.id
+                                ));
+
+                            match self.state {
+                                ParserState::Lemma => {
+                                    lemma.grammemes.push(grammeme);
+                                }
+
+                                ParserState::Variation => {
+                                    lemma
+                                        .variations
+                                        .last_mut()
+                                        .ensure("variations should be non-empty")
+                                        .grammemes
+                                        .push(grammeme);
+                                }
+
+                                _ => bail(format!("invalid parser state: encountered grammeme association while in {:?}", self.state)),
+                            }
+                        }
+
+                        other => bail(format!("unexpected element while parsing lemma: {other}")),
+                    };
+                }
+
+                XmlEvent::EndElement { name } => match name.local_name.as_str() {
+                    "l" if self.state == ParserState::Lemma => continue,
+                    "f" if self.state == ParserState::Variation => {
+                        self.state = ParserState::Lemma;
+                        continue;
+                    }
+                    "g" => continue,
+                    other => bail(format!(
+                        "unexpected </{other}> while parsing lemma {}",
+                        lemma.id
+                    )),
+                },
+
+                _ => continue,
+            }
+        }
+    }
+
+    fn parse_link_type(&mut self, name: &OwnedName, attributes: &[OwnedAttribute]) -> LinkType {
+        if name.local_name != "type" {
+            bail(format!(
+                "expected to parse a link type, but found <{}>",
+                name.local_name
+            ));
+        }
+
+        let mut link_type = LinkType::default();
+
+        for attr in attributes {
+            if attr.name.local_name == "id" {
+                link_type.id = u64::from_str(&attr.value).ensure("failed to parse link type ID");
+            }
+        }
+
+        link_type.name = self.parse_string("type");
+        link_type
+    }
+
+    fn parse_link(&mut self, attributes: &[OwnedAttribute]) -> Link {
+        let mut link = Link::default();
+
+        for attr in attributes {
+            let i_val = || u64::from_str(&attr.value).ensure("failed to parse link field");
+
+            match attr.name.local_name.as_str() {
+                "id" => {
+                    link.id = i_val();
+                }
+                "from" => {
+                    link.from = i_val();
+                }
+                "to" => {
+                    link.to = i_val();
+                }
+                "type" => {
+                    link.link_type = i_val();
+                }
+
+                other => {
+                    warn!("unexpected attribute {} on <link>", other);
+                    continue;
+                }
+            }
+        }
+
+        // expect the end of the <link> element, though since these
+        // are empty it should be immediate.
+        self.skip_section("link");
+
+        link
+    }
+}
diff --git a/corp/russian/data-import/src/or_parser.rs b/corp/russian/data-import/src/or_parser.rs
new file mode 100644
index 000000000000..8bfc61dbef48
--- /dev/null
+++ b/corp/russian/data-import/src/or_parser.rs
@@ -0,0 +1,105 @@
+//! Parser for the OpenRussian data format.
+//!
+//! Note that when exporting OpenRussian data from the project you
+//! have to choose an encoding. We choose tab-separated CSV files, as
+//! tabs have a very low probability of actually appearing in the
+//! input data and this skips some potential encoding issues.
+
+use super::Ensure;
+use serde::Deserialize;
+use std::fs::File;
+use std::io::BufReader;
+use std::path::PathBuf;
+
+/// A word from the `words` table.
+#[derive(Debug, Deserialize)]
+pub struct Word {
+    pub id: usize,
+    pub position: String, // TODO: unknown
+    pub bare: String,     // TODO: unknown
+    pub accented: String, // TODO: unknown
+    pub derived_from_word_id: Option<usize>,
+    pub rank: Option<usize>,
+    pub disabled: String,     // TODO: unknown
+    pub audio: String,        // TODO: unknown
+    pub usage_en: String,     // TODO: unknown
+    pub usage_de: String,     // TODO: unknown
+    pub number_value: String, // TODO: unknown
+
+    #[serde(rename = "type")]
+    pub word_type: String, // TODO: unknown
+
+    pub level: String,      // TODO: unknown
+    pub created_at: String, // TODO: unknown
+}
+
+/// A word form from the `words_forms` table.
+#[derive(Debug, Deserialize)]
+pub struct WordForm {
+    pub id: usize,
+    pub word_id: usize,
+    pub form_type: String,
+    pub position: String,
+    pub form: String,
+    pub form_bare: String,
+}
+
+/// A translation from the `translations` table.
+#[derive(Debug, Deserialize)]
+pub struct Translation {
+    pub id: usize,
+    pub lang: String,
+    pub word_id: usize,
+    pub position: String,
+    pub tl: String, // unknown
+    pub example_ru: String,
+    pub example_tl: String,
+    pub info: String,
+}
+
+pub struct OpenRussianParser {
+    or_directory: PathBuf,
+}
+
+pub type DynIter<T> = Box<dyn Iterator<Item = T>>;
+
+impl OpenRussianParser {
+    pub fn new<P: Into<PathBuf>>(path: P) -> Self {
+        OpenRussianParser {
+            or_directory: path.into(),
+        }
+    }
+
+    pub fn words(&self) -> DynIter<Word> {
+        self.parser_for("words.csv")
+    }
+
+    pub fn words_forms(&self) -> DynIter<WordForm> {
+        self.parser_for("words_forms.csv")
+    }
+
+    pub fn translations(&self) -> DynIter<Translation> {
+        self.parser_for("translations.csv")
+    }
+
+    fn parser_for<T: serde::de::DeserializeOwned + 'static>(
+        &self,
+        file_name: &str,
+    ) -> Box<dyn Iterator<Item = T>> {
+        let mut path = self.or_directory.clone();
+        path.push(file_name);
+
+        let reader = csv::ReaderBuilder::new()
+            .delimiter(b'\t')
+            .from_reader(BufReader::new(
+                File::open(&path).ensure("failed to open words.csv"),
+            ));
+
+        Box::new(reader.into_deserialize().map(|result| {
+            result.ensure(format!(
+                "failed to deserialize {}",
+                std::any::type_name::<T>()
+            ))
+        }))
+    }
+}
diff --git a/corp/russian/predlozhnik/.gitignore b/corp/russian/predlozhnik/.gitignore
new file mode 100644
index 000000000000..58eaf3e32687
--- /dev/null
+++ b/corp/russian/predlozhnik/.gitignore
@@ -0,0 +1,3 @@
+/target/
+**/*.rs.bk
+dist/
diff --git a/corp/russian/predlozhnik/Cargo.lock b/corp/russian/predlozhnik/Cargo.lock
new file mode 100644
index 000000000000..81fcd51ced29
--- /dev/null
+++ b/corp/russian/predlozhnik/Cargo.lock
@@ -0,0 +1,460 @@
+# 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 = "boolinator"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
+
+[[package]]
+name = "bumpalo"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23947965eee55e3e97a5cd142dd4c10631cc349b48cecca0ed230fd296f568cd"
+dependencies = [
+ "gloo-console",
+ "gloo-dialogs",
+ "gloo-events",
+ "gloo-file",
+ "gloo-render",
+ "gloo-storage",
+ "gloo-timers",
+ "gloo-utils",
+]
+
+[[package]]
+name = "gloo-console"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-dialogs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-events"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
+dependencies = [
+ "gloo-events",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-render"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40913a05c8297adca04392f707b1e73b12ba7b8eab7244a4961580b1fd34063c"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+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 = "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 = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[package]]
+name = "predlozhnik"
+version = "0.1.0"
+dependencies = [
+ "lazy_static",
+ "maplit",
+ "wasm-bindgen",
+ "yew",
+]
+
+[[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.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "scoped-tls-hkt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
+
+[[package]]
+name = "serde"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "web-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "yew"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a1ccb53e57d3f7d847338cf5758befa811cabe207df07f543c06f502f9998cd"
+dependencies = [
+ "console_error_panic_hook",
+ "gloo",
+ "gloo-utils",
+ "indexmap",
+ "js-sys",
+ "scoped-tls-hkt",
+ "slab",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "yew-macro",
+]
+
+[[package]]
+name = "yew-macro"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fab79082b556d768d6e21811869c761893f0450e1d550a67892b9bce303b7bb"
+dependencies = [
+ "boolinator",
+ "lazy_static",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/corp/russian/predlozhnik/Cargo.toml b/corp/russian/predlozhnik/Cargo.toml
new file mode 100644
index 000000000000..e00d46b5eb30
--- /dev/null
+++ b/corp/russian/predlozhnik/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "predlozhnik"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+maplit = "1.0"
+lazy_static = "1.4"
+yew = "0.19"
+
+# needs to be in sync with nixpkgs
+wasm-bindgen = "= 0.2.84"
diff --git a/corp/russian/predlozhnik/default.nix b/corp/russian/predlozhnik/default.nix
new file mode 100644
index 000000000000..2137be111278
--- /dev/null
+++ b/corp/russian/predlozhnik/default.nix
@@ -0,0 +1,52 @@
+{ lib, pkgs, ... }:
+
+let
+  wasmRust = pkgs.rust-bin.stable.latest.default.override {
+    targets = [ "wasm32-unknown-unknown" ];
+  };
+
+  cargoToml = with builtins; fromTOML (readFile ./Cargo.toml);
+
+  wasmBindgenMatch =
+    cargoToml.dependencies.wasm-bindgen == "= ${pkgs.wasm-bindgen-cli.version}";
+
+  assertWasmBindgen = assert (lib.assertMsg wasmBindgenMatch ''
+    Due to instability in the Rust WASM ecosystem, the trunk build
+    tool enforces that the Cargo-dependency version of `wasm-bindgen`
+    MUST match the version of the CLI supplied in the environment.
+
+    This can get out of sync when nixpkgs is updated. To resolve it,
+    wasm-bindgen must be bumped in the Cargo.toml file and cargo needs
+    to be run to resolve the dependencies.
+
+    Versions of `wasm-bindgen` in Cargo.toml:
+
+      Expected: '= ${pkgs.wasm-bindgen-cli.version}'
+      Actual:   '${cargoToml.dependencies.wasm-bindgen}'
+  ''); pkgs.wasm-bindgen-cli;
+
+  deps = with pkgs; [
+    binaryen
+    sass
+    wasmRust
+    trunk
+    assertWasmBindgen
+  ];
+
+in
+pkgs.rustPlatform.buildRustPackage rec {
+  pname = "predlozhnik";
+  version = "canon";
+  src = lib.cleanSource ./.;
+  cargoLock.lockFile = ./Cargo.lock;
+
+  buildPhase = ''
+    export PATH=${lib.makeBinPath deps}:$PATH
+    mkdir home
+    export HOME=$PWD/.home
+    env
+    trunk build --release -d $out
+  '';
+
+  dontInstall = true;
+}
diff --git a/corp/russian/predlozhnik/index.css b/corp/russian/predlozhnik/index.css
new file mode 100644
index 000000000000..3529574c4f2b
--- /dev/null
+++ b/corp/russian/predlozhnik/index.css
@@ -0,0 +1,29 @@
+body {
+    max-width: 800px;
+    margin: 40px auto;
+}
+
+#header {
+    display: flex;
+    flex-direction: column;
+}
+
+.btn.btn-ghost:disabled {
+    border-color: #9f9f9f;
+    color: #9f9f9f;
+}
+
+#predlogi,#padezhi {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+}
+
+.btn {
+    margin: 3px;
+    flex-grow: 1;
+}
+
+.footer {
+    text-align: right;
+}
diff --git a/corp/russian/predlozhnik/index.html b/corp/russian/predlozhnik/index.html
new file mode 100644
index 000000000000..6af1adc0bfba
--- /dev/null
+++ b/corp/russian/predlozhnik/index.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet"
+          href="https://unpkg.com/terminal.css@0.7.2/dist/terminal.min.css" />
+    <link data-trunk rel="inline" href="index.css">
+    <title>Предложник</title>
+
+    <!-- Yandex.RTB -->
+    <script>window.yaContextCb=window.yaContextCb||[]</script>
+    <script src="https://yandex.ru/ads/system/context.js" async></script>
+  </head>
+  <body>
+    <noscript>
+      <h1>Предложник</h1>
+      <p>
+        ... показывает с какими падежами употребляются предлоги в
+        русском языке. Но, к сожалению, только с помощью Javascript.
+      </p>
+    </noscript>
+  </body>
+</html>
diff --git a/corp/russian/predlozhnik/src/main.rs b/corp/russian/predlozhnik/src/main.rs
new file mode 100644
index 000000000000..56ff04808f9d
--- /dev/null
+++ b/corp/russian/predlozhnik/src/main.rs
@@ -0,0 +1,345 @@
+use yew::html::Scope;
+use yew::prelude::*;
+
+use lazy_static::lazy_static;
+use maplit::hashmap;
+use std::collections::BTreeSet;
+use std::collections::HashMap;
+
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
+enum Падеж {
+    Именительный,
+    Родительный,
+    Дательный,
+    Винительный,
+    Творительный,
+    Предложный,
+}
+
+impl Падеж {
+    const ВСЕ: [Self; 6] = [
+        Self::Именительный,
+        Self::Родительный,
+        Self::Дательный,
+        Self::Винительный,
+        Self::Творительный,
+        Self::Предложный,
+    ];
+
+    fn вопрос(&self) -> &str {
+        use Падеж::*;
+        match self {
+            Именительный => "кто? Что?",
+            Родительный => "кого? Чего?",
+            Дательный => "кому? Чему?",
+            Винительный => "кого? Что?",
+            Творительный => "кем? Чем?",
+            Предложный => "ком? Чём?",
+        }
+    }
+}
+
+lazy_static! {
+    static ref ПО_ПРЕДЛОГУ: HashMap<&'static str, BTreeSet<Падеж>> = {
+        use Падеж::*;
+
+        hashmap! {
+            "без" => BTreeSet::from([Родительный]),
+            "близ" => BTreeSet::from([Родительный]),
+            "в" => BTreeSet::from([Винительный, Предложный]),
+            "вместо" => BTreeSet::from([Родительный]),
+            "вне" => BTreeSet::from([Родительный]),
+            "внутри" => BTreeSet::from([Родительный]),
+            "возле" => BTreeSet::from([Родительный]),
+            "вокруг" => BTreeSet::from([Родительный]),
+            "вроде" => BTreeSet::from([Родительный]),
+            "для" => BTreeSet::from([Родительный]),
+            "до" => BTreeSet::from([Родительный]),
+            "за" => BTreeSet::from([Винительный, Творительный]),
+            "из" => BTreeSet::from([Родительный]),
+            "из-за" => BTreeSet::from([Родительный]),
+            "из-под" => BTreeSet::from([Родительный]),
+            "к" => BTreeSet::from([Дательный]),
+            "кроме" => BTreeSet::from([Родительный]),
+            "между" => BTreeSet::from([Творительный, Родительный]),
+            "на" => BTreeSet::from([Винительный, Предложный]),
+            "над" => BTreeSet::from([Творительный]),
+            "нет" => BTreeSet::from([Родительный]),
+            "о" => BTreeSet::from([Винительный, Предложный]),
+            "около" => BTreeSet::from([Родительный]),
+            "от" => BTreeSet::from([Родительный]),
+            "перед" => BTreeSet::from([Творительный]),
+            "по" => BTreeSet::from([Винительный, Дательный, Предложный]),
+            "под" => BTreeSet::from([Винительный, Творительный]),
+            "после" => BTreeSet::from([Родительный]),
+            "при" => BTreeSet::from([Предложный]),
+            "про" => BTreeSet::from([Винительный]),
+            "ради" => BTreeSet::from([Родительный]),
+            "с" => BTreeSet::from([Родительный, Винительный, Творительный]),
+            "сквозь" => BTreeSet::from([Винительный]),
+            "среди" => BTreeSet::from([Родительный]),
+            "у" => BTreeSet::from([Родительный]),
+            "через" => BTreeSet::from([Винительный]),
+        }
+    };
+    static ref ПО_ПАДЕЖУ: HashMap<Падеж, BTreeSet<&'static str>> = {
+        let mut m = hashmap!();
+
+        for c in Падеж::ВСЕ {
+            let mut предлоги: BTreeSet<&'static str> = BTreeSet::new();
+            for (k, v) in &*ПО_ПРЕДЛОГУ {
+                if v.contains(&c) {
+                    предлоги.insert(k);
+                }
+            }
+
+            m.insert(c, предлоги);
+        }
+
+        m
+    };
+    static ref ПАДЕЖИ: BTreeSet<Падеж> = BTreeSet::from(Падеж::ВСЕ);
+    static ref ПРЕДЛОГИ: BTreeSet<&'static str> = {
+        let mut s: BTreeSet<&'static str> = BTreeSet::new();
+
+        for п in ПО_ПРЕДЛОГУ.keys() {
+            s.insert(п);
+        }
+
+        s
+    };
+}
+
+fn исключение(предлог: &str, падеж: Падеж) -> Option<Html> {
+    use Падеж::*;
+
+    match (предлог, падеж) {
+        ("в", Винительный) => Some(html! {"Во что? В кого?"}),
+
+        ("о", Винительный) => Some(html! {
+            <>
+              <p>{"О кого? Обо что?"}</p>
+              <p>{"Редко используется. Например:"}</p>
+              <ul>
+                <li>{"Удариться о притолоку."}</li>
+                <li>{"точить о камень."}</li>
+              </ul>
+            </>
+        }),
+
+        ("между", Родительный) => Some(html! {
+            <>
+              <p>{"Между чего?"}</p>
+              <p>{"Редко используется. Только в идиомах и старой литературе:"}</p>
+              <ul>
+                <li>{"Читаю между строк."}</li>
+              </ul>
+            </>
+        }),
+
+        _ => None,
+    }
+}
+
+enum Сообщение {
+    ВыбралПадеж(Option<Падеж>),
+    ВыбралПредлог(Option<&'static str>),
+}
+
+#[derive(Default)]
+struct Модель {
+    падеж: Option<Падеж>,
+    предлог: Option<&'static str>,
+}
+
+struct Вывод {
+    доступные_падежи: BTreeSet<Падеж>,
+    доступные_предлоги: BTreeSet<&'static str>,
+    объяснение: Option<Html>,
+}
+
+fn объясни(падеж: Падеж, предлог: &str) -> Html {
+    let иск = match исключение(предлог, падеж) {
+        Some(exp) => html! { exp },
+        None => html! { format!("{} {}", предлог, падеж.вопрос()) },
+    };
+
+    html! {
+        <div id="obyasnenie">
+          <hr/>
+          <h2>{"Пример:"}</h2>
+          {иск}
+        </div>
+    }
+}
+
+fn ограничить(м: &Модель) -> Вывод {
+    match (м.падеж, &м.предлог) {
+        (Some(пж), Some(пл)) => Вывод {
+            доступные_падежи: (*ПО_ПРЕДЛОГУ)[пл].clone(),
+            доступные_предлоги: (*ПО_ПАДЕЖУ)[&пж].clone(),
+            объяснение: Some(объясни(пж, пл)),
+        },
+
+        (Some(пж), None) => Вывод {
+            доступные_падежи: BTreeSet::from([пж]),
+            доступные_предлоги: (*ПО_ПАДЕЖУ)[&пж].clone(),
+            объяснение: None,
+        },
+
+        (None, Some(пл)) => Вывод {
+            доступные_падежи: (*ПО_ПРЕДЛОГУ)[пл].clone(),
+            доступные_предлоги: BTreeSet::from([*пл]),
+            объяснение: None,
+        },
+
+        (None, None) => Вывод {
+            доступные_падежи: ПАДЕЖИ.clone(),
+            доступные_предлоги: ПРЕДЛОГИ.clone(),
+            объяснение: None,
+        },
+    }
+}
+
+fn класс_кнопки(выбран: bool, доступен: bool) -> String {
+    let класс = "btn ".to_string();
+    класс
+        + match (выбран, доступен) {
+            (true, _) => "btn-primary",
+            (false, true) => "btn-ghost btn-primary",
+            (false, false) => "btn-ghost btn-default",
+        }
+}
+
+fn покажи_предлог(
+    link: &Scope<Модель>,
+    м: &Модель,
+    вв: &Вывод,
+    п: &'static str,
+) -> Html {
+    let выбран = м.предлог == Some(п);
+    let доступен = вв.доступные_предлоги.contains(п);
+    let класс = класс_кнопки(выбран, доступен);
+
+    html! {
+        <button class={класс}
+         onclick={link.callback(move |_| if выбран {
+             Сообщение::ВыбралПредлог(None)
+         } else {
+             Сообщение::ВыбралПредлог(Some(п))
+         })}
+         disabled={!доступен}>
+        {п}
+        </button>
+    }
+}
+
+fn покажи_падеж(
+    link: &Scope<Модель>, м: &Модель, вв: &Вывод, п: Падеж
+) -> Html {
+    let выбран = м.падеж == Some(п);
+    let доступен = вв.доступные_падежи.contains(&п);
+    let класс = класс_кнопки(выбран, доступен);
+
+    html! {
+        <button class={класс}
+         onclick={link.callback(move |_| if выбран {
+             Сообщение::ВыбралПадеж(None)
+         } else {
+             Сообщение::ВыбралПадеж(Some(п))
+         })}
+         disabled={!доступен}>
+        {format!("{:?}", п)}
+        </button>
+    }
+}
+
+impl Component for Модель {
+    type Message = Сообщение;
+    type Properties = ();
+
+    fn create(_ctx: &Context<Self>) -> Self {
+        Default::default()
+    }
+
+    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Сообщение::ВыбралПадеж(пж) => self.падеж = пж,
+            Сообщение::ВыбралПредлог(пл) => self.предлог = пл,
+        }
+
+        true
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let вв = ограничить(self);
+        let link = ctx.link();
+
+        let кнопки_предлогов = ПРЕДЛОГИ
+            .iter()
+            .map(|п| покажи_предлог(link, self, &вв, п))
+            .collect::<Html>();
+
+        let кнопки_падежов = ПАДЕЖИ
+            .iter()
+            .map(|п| покажи_падеж(link, self, &вв, *п))
+            .collect::<Html>();
+
+        let объяснение = вв.объяснение.map(|exp| exp).unwrap_or_else(|| html! {});
+
+        let footer = html! {
+            <footer>
+              <hr/>
+              <p class="footer">
+                <a href="https://code.tvl.fyi/tree/corp/russian/predlozhnik">{"код"}</a>
+                {" | "}
+                {"сделано "}<a href="https://tvl.su">{"ООО \"ТВЛ\""}</a>
+              </p>
+            </footer>
+        };
+
+        let код_рекламы = r#"
+window.yaContextCb.push(()=>{
+  Ya.Context.AdvManager.render({
+    renderTo: 'yandex_rtb_R-A-1773485-1',
+    blockId: 'R-A-1773485-1'
+  })
+})
+"#;
+
+        let реклама = html! {
+            <div id="ad">
+              <div id="yandex_rtb_R-A-1773485-1"></div>
+              <script>{код_рекламы}</script>
+            </div>
+        };
+
+        html! {
+            <>
+                <div id="header">
+                  <h1>{"Предложник"}</h1>
+                  <p>{"... показывает с какими падежами употребляются предлоги в русском языке."}</p>
+                </div>
+
+                <h2>{"Выбирай предлог:"}</h2>
+                <div id="predlogi">
+                  {кнопки_предлогов}
+                </div>
+                <hr/>
+
+                <h2>{"Выбирай падеж:"}</h2>
+                <div id="padezhi">
+                  {кнопки_падежов}
+                </div>
+
+                {объяснение}
+                {footer}
+                {реклама}
+            </>
+        }
+    }
+}
+
+fn main() {
+    yew::start_app::<Модель>();
+}
diff --git a/corp/tvixbolt/.gitignore b/corp/tvixbolt/.gitignore
new file mode 100644
index 000000000000..77fdd3e5cfc1
--- /dev/null
+++ b/corp/tvixbolt/.gitignore
@@ -0,0 +1,2 @@
+/target
+dist
diff --git a/corp/tvixbolt/Cargo.lock b/corp/tvixbolt/Cargo.lock
new file mode 100644
index 000000000000..d12d9ce96387
--- /dev/null
+++ b/corp/tvixbolt/Cargo.lock
@@ -0,0 +1,1036 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+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",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitmaps"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403"
+
+[[package]]
+name = "boolinator"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
+
+[[package]]
+name = "bumpalo"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "codemap"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
+
+[[package]]
+name = "codemap-diagnostic"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ba0e6be8e2774e750f9e90625b490249715bece38a12f9d09e82477caba5028"
+dependencies = [
+ "atty",
+ "codemap",
+ "termcolor",
+]
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "countme"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
+
+[[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 = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
+
+[[package]]
+name = "genawaiter"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
+dependencies = [
+ "genawaiter-macro",
+]
+
+[[package]]
+name = "genawaiter-macro"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gloo"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23947965eee55e3e97a5cd142dd4c10631cc349b48cecca0ed230fd296f568cd"
+dependencies = [
+ "gloo-console",
+ "gloo-dialogs",
+ "gloo-events",
+ "gloo-file",
+ "gloo-render",
+ "gloo-storage",
+ "gloo-timers",
+ "gloo-utils",
+]
+
+[[package]]
+name = "gloo-console"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-dialogs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-events"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
+dependencies = [
+ "futures-channel",
+ "gloo-events",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-render"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8e8fc851e9c7b9852508bc6e3f690f452f474417e8545ec9857b7f7377036b5"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "imbl"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2806b69cd9f4664844027b64465eacb444c67c1db9c778e341adff0c25cdb0d"
+dependencies = [
+ "bitmaps",
+ "imbl-sized-chunks",
+ "rand_core",
+ "rand_xoshiro",
+ "serde",
+ "version_check",
+]
+
+[[package]]
+name = "imbl-sized-chunks"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6957ea0b2541c5ca561d3ef4538044af79f8a05a1eb3a3b148936aaceaa1076"
+dependencies = [
+ "bitmaps",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+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 = "lexical-core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46"
+dependencies = [
+ "lexical-parse-float",
+ "lexical-parse-integer",
+ "lexical-util",
+ "lexical-write-float",
+ "lexical-write-integer",
+]
+
+[[package]]
+name = "lexical-parse-float"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
+dependencies = [
+ "lexical-parse-integer",
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-parse-integer"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
+dependencies = [
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-util"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
+dependencies = [
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-write-float"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862"
+dependencies = [
+ "lexical-util",
+ "lexical-write-integer",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-write-integer"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446"
+dependencies = [
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "nom8"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "path-clean"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[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.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rand_xoshiro"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+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.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rnix"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f"
+dependencies = [
+ "rowan",
+]
+
+[[package]]
+name = "route-recognizer"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
+
+[[package]]
+name = "rowan"
+version = "0.15.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5811547e7ba31e903fe48c8ceab10d40d70a101f3d15523c847cce91aa71f332"
+dependencies = [
+ "countme",
+ "hashbrown",
+ "memoffset",
+ "rustc-hash",
+ "text-size",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "ryu"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+
+[[package]]
+name = "scoped-tls-hkt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
+
+[[package]]
+name = "serde"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "618365e8e586c22123d692b72a7d791d5ee697817b65a218cdf12a98870af0f7"
+dependencies = [
+ "fnv",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smol_str"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tabwriter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36205cfc997faadcc4b0b87aaef3fbedafe20d38d4959a7ca6ff803564051111"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "text-size"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a"
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
+dependencies = [
+ "indexmap",
+ "nom8",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+]
+
+[[package]]
+name = "tvix-eval"
+version = "0.1.0"
+dependencies = [
+ "codemap",
+ "codemap-diagnostic",
+ "dirs",
+ "genawaiter",
+ "imbl",
+ "lazy_static",
+ "lexical-core",
+ "path-clean",
+ "regex",
+ "rnix",
+ "rowan",
+ "serde",
+ "serde_json",
+ "smol_str",
+ "tabwriter",
+ "toml",
+ "tvix-eval-builtin-macros",
+ "xml-rs",
+]
+
+[[package]]
+name = "tvix-eval-builtin-macros"
+version = "0.0.1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tvixbolt"
+version = "0.1.0"
+dependencies = [
+ "codemap",
+ "rnix",
+ "serde",
+ "serde_urlencoded",
+ "tvix-eval",
+ "wasm-bindgen",
+ "web-sys",
+ "yew",
+ "yew-router",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[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.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "web-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+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 = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+
+[[package]]
+name = "yew"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a1ccb53e57d3f7d847338cf5758befa811cabe207df07f543c06f502f9998cd"
+dependencies = [
+ "console_error_panic_hook",
+ "gloo",
+ "gloo-utils",
+ "indexmap",
+ "js-sys",
+ "scoped-tls-hkt",
+ "slab",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "yew-macro",
+]
+
+[[package]]
+name = "yew-macro"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fab79082b556d768d6e21811869c761893f0450e1d550a67892b9bce303b7bb"
+dependencies = [
+ "boolinator",
+ "lazy_static",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "yew-router"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "155804f6f3aa309f596d5c3fa14486a94e7756f1edd7634569949e401d5099f2"
+dependencies = [
+ "gloo",
+ "gloo-utils",
+ "js-sys",
+ "route-recognizer",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_urlencoded",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+ "yew",
+ "yew-router-macro",
+]
+
+[[package]]
+name = "yew-router-macro"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39049d193b52eaad4ffc80916bf08806d142c90b5edcebd527644de438a7e19a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/corp/tvixbolt/Cargo.toml b/corp/tvixbolt/Cargo.toml
new file mode 100644
index 000000000000..d0aa6c77360a
--- /dev/null
+++ b/corp/tvixbolt/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "tvixbolt"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+yew = "0.19.3"
+yew-router = "0.16"
+codemap = "0.1.3"
+serde_urlencoded = "*" # pinned by yew
+rnix = "0.11.0"
+
+# needs to be in sync with nixpkgs
+wasm-bindgen = "= 0.2.84"
+
+[dependencies.tvix-eval]
+path = "../../tvix/eval"
+default-features = false
+
+[dependencies.serde]
+version = "*" # pinned by yew
+features = [ "derive" ]
+
+[dependencies.web-sys]
+version = "*" # pinned by yew
+features = [ "HtmlDetailsElement" ]
diff --git a/corp/tvixbolt/default.nix b/corp/tvixbolt/default.nix
new file mode 100644
index 000000000000..9b6baa582fb0
--- /dev/null
+++ b/corp/tvixbolt/default.nix
@@ -0,0 +1,74 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  wasmRust = pkgs.rust-bin.stable.latest.default.override {
+    targets = [ "wasm32-unknown-unknown" ];
+  };
+
+  cargoToml = with builtins; fromTOML (readFile ./Cargo.toml);
+
+  wasmBindgenMatch =
+    cargoToml.dependencies.wasm-bindgen == "= ${pkgs.wasm-bindgen-cli.version}";
+
+  assertWasmBindgen = assert (lib.assertMsg wasmBindgenMatch ''
+    Due to instability in the Rust WASM ecosystem, the trunk build
+    tool enforces that the Cargo-dependency version of `wasm-bindgen`
+    MUST match the version of the CLI supplied in the environment.
+
+    This can get out of sync when nixpkgs is updated. To resolve it,
+    wasm-bindgen must be bumped in the Cargo.toml file and cargo needs
+    to be run to resolve the dependencies.
+
+    Versions of `wasm-bindgen` in Cargo.toml:
+
+      Expected: '= ${pkgs.wasm-bindgen-cli.version}'
+      Actual:   '${cargoToml.dependencies.wasm-bindgen}'
+  ''); pkgs.wasm-bindgen-cli;
+
+  deps = [
+    pkgs.binaryen
+    pkgs.sass
+    pkgs.trunk
+
+    wasmRust
+    assertWasmBindgen
+  ];
+
+  # Cargo.toml needs to be patched with the /nix/store source path of
+  # tvix-eval.
+  cargoTomlPatch = pkgs.writeText "tvix-eval-src.patch" ''
+    diff --git a/Cargo.toml b/Cargo.toml
+    index 75006bec18..6ca244bbb2 100644
+    --- a/Cargo.toml
+    +++ b/Cargo.toml
+    @@ -16,7 +16,7 @@ rnix = "0.11.0"
+     wasm-bindgen = "= 0.2.83"
+
+     [dependencies.tvix-eval]
+    -path = "../../tvix/eval"
+    +path = "${depot.tvix.crates.workspaceMembers.tvix-eval.build.src}"
+     default-features = false
+
+     [dependencies.serde]
+  '';
+in
+pkgs.rustPlatform.buildRustPackage rec {
+  pname = "tvixbolt";
+  version = "canon";
+  src = lib.cleanSource ./.;
+
+  cargoLock.lockFile = ./Cargo.lock;
+
+  patches = [
+    cargoTomlPatch
+  ];
+
+  buildPhase = ''
+    export PATH=${lib.makeBinPath deps}:$PATH
+    mkdir home
+    export HOME=$PWD/home
+    trunk build --release -d $out
+  '';
+
+  dontInstall = true;
+}
diff --git a/corp/tvixbolt/index.css b/corp/tvixbolt/index.css
new file mode 100644
index 000000000000..95bd7d098362
--- /dev/null
+++ b/corp/tvixbolt/index.css
@@ -0,0 +1,7 @@
+.footer {
+    text-align: right;
+}
+
+.lod {
+    text-align: center;
+}
diff --git a/corp/tvixbolt/index.html b/corp/tvixbolt/index.html
new file mode 100644
index 000000000000..62bf14539f2b
--- /dev/null
+++ b/corp/tvixbolt/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet"
+          href="https://unpkg.com/terminal.css@0.7.2/dist/terminal.min.css" />
+    <link data-trunk rel="inline" href="index.css">
+    <title>Tvixbolt</title>
+
+    <!-- Yandex.RTB -->
+    <script>window.yaContextCb=window.yaContextCb||[]</script>
+    <script src="https://yandex.ru/ads/system/context.js" async></script>
+  </head>
+</html>
diff --git a/corp/tvixbolt/src/main.rs b/corp/tvixbolt/src/main.rs
new file mode 100644
index 000000000000..0de48b3ac71e
--- /dev/null
+++ b/corp/tvixbolt/src/main.rs
@@ -0,0 +1,342 @@
+use std::fmt::Write;
+
+use serde::{Deserialize, Serialize};
+use tvix_eval::observer::{DisassemblingObserver, TracingObserver};
+use web_sys::HtmlDetailsElement;
+use web_sys::HtmlTextAreaElement;
+use yew::prelude::*;
+use yew::TargetCast;
+use yew_router::{prelude::*, AnyRoute};
+
+#[derive(Clone)]
+enum Msg {
+    CodeChange(String),
+    ToggleTrace(bool),
+    ToggleDisplayAst(bool),
+
+    // Required because browsers are stupid and it's easy to get into
+    // infinite loops with `ontoggle` events.
+    NoOp,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+struct Model {
+    code: String,
+
+    // #[serde(skip_serializing)]
+    trace: bool,
+
+    // #[serde(skip_serializing)]
+    display_ast: bool,
+}
+
+fn tvixbolt_overview() -> Html {
+    html! {
+        <>
+          <p>
+            {"This page lets you explore the bytecode generated by the "}
+            <a href="https://cs.tvl.fyi/depot/-/tree/tvix">{"Tvix"}</a>
+            {" compiler for the Nix language. See the "}
+            <a href="https://tvl.fyi/blog/rewriting-nix">{"Tvix announcement"}</a>
+            {" for some background information on Tvix itself."}
+          </p>
+          <p>
+            {"Tvix is still "}<i>{"extremely work-in-progress"}</i>{" and you "}
+            {"should expect to be able to cause bugs and errors in this tool."}
+          </p>
+          <p>
+            {"Tvixbolt is a project from "}
+            <a href="https://tvl.su">
+              {"TVL LLC"}
+            </a>
+            {". If you're looking for the TVL Community, click "}
+            <a href="https://tvl.fyi">
+              {"here"}
+            </a>
+            {"."}
+          </p>
+        </>
+    }
+}
+
+/// This renders an ad in the Tvixbolt footer. Most people that end up
+/// on Tvixbolt will probably block this anyways, but might as well.
+fn ad() -> Html {
+    let ad_code = r#"
+window.yaContextCb.push(()=>{
+  Ya.Context.AdvManager.render({
+    renderTo: 'yandex_rtb_R-A-1943274-1',
+    blockId: 'R-A-1943274-1'
+  })
+})
+"#;
+
+    html! {
+        <div id="ad">
+            <div id="yandex_rtb_R-A-1943274-1"></div>
+            <script>{ad_code}</script>
+        </div>
+    }
+}
+
+fn footer_link(location: &'static str, name: &str) -> Html {
+    html! {
+        <>
+            <a class="uncoloured-link" href={location}>{name}</a>{" | "}
+        </>
+    }
+}
+
+fn footer() -> Html {
+    html! {
+        <>
+        <hr/>
+        <footer>
+          <p class="footer">
+            {footer_link("https://tvl.su", "home")}
+            {footer_link("https://cs.tvl.fyi", "code")}
+            {footer_link("https://tvl.fyi/builds", "ci")}
+            {footer_link("https://b.tvl.fyi", "bugs")}
+            {"© ООО ТВЛ"}
+          </p>
+          <p class="lod">{"ಠ_ಠ"}</p>
+          {ad()}
+        </footer>
+        </>
+    }
+}
+
+impl Component for Model {
+    type Message = Msg;
+    type Properties = ();
+
+    fn create(_: &Context<Self>) -> Self {
+        BrowserHistory::new()
+            .location()
+            .query::<Self>()
+            .unwrap_or_else(|_| Self {
+                code: String::new(),
+                trace: false,
+                display_ast: false,
+            })
+    }
+
+    fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Msg::ToggleTrace(trace) => {
+                self.trace = trace;
+            }
+
+            Msg::ToggleDisplayAst(display_ast) => {
+                self.display_ast = display_ast;
+            }
+
+            Msg::CodeChange(new_code) => {
+                self.code = new_code;
+            }
+
+            Msg::NoOp => {}
+        }
+
+        let _ = BrowserHistory::new().replace_with_query(AnyRoute::new("/"), self.clone());
+
+        true
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        // This gives us a component's "`Scope`" which allows us to send messages, etc to the component.
+        let link = ctx.link();
+        html! {
+            <>
+            <div class="container">
+                <h1>{"tvixbolt 0.1-alpha"}</h1>
+                {tvixbolt_overview()}
+                <form>
+                  <fieldset>
+                    <legend>{"Input"}</legend>
+
+                    <div class="form-group">
+                        <label for="code">{"Nix code:"}</label>
+                        <textarea
+                         oninput={link.callback(|e: InputEvent| {
+                             let ta = e.target_unchecked_into::<HtmlTextAreaElement>().value();
+                             Msg::CodeChange(ta)
+
+                         })}
+                         id="code" cols="30" rows="10" value={self.code.clone()}>
+                         </textarea>
+                    </div>
+                  </fieldset>
+                </form>
+                <hr />
+                {self.run(ctx)}
+                {footer()}
+            </div>
+            </>
+        }
+    }
+}
+
+impl Model {
+    fn run(&self, ctx: &Context<Self>) -> Html {
+        if self.code.is_empty() {
+            return html! {
+                <p>
+                  {"Enter some Nix code above to get started. Don't know Nix yet? "}
+                  {"Check out "}
+                  <a href="https://code.tvl.fyi/about/nix/nix-1p/README.md">{"nix-1p"}</a>
+                  {"!"}
+                </p>
+            };
+        }
+
+        html! {
+            <>
+              <h2>{"Result:"}</h2>
+            {eval(self).display(ctx, self)}
+            </>
+        }
+    }
+}
+
+#[derive(Default)]
+struct Output {
+    errors: String,
+    warnings: String,
+    output: String,
+    bytecode: Vec<u8>,
+    trace: Vec<u8>,
+    ast: String,
+}
+
+fn maybe_show(title: &str, s: &str) -> Html {
+    if s.is_empty() {
+        html! {}
+    } else {
+        html! {
+            <>
+              <h3>{title}</h3>
+              <pre>{s}</pre>
+            </>
+        }
+    }
+}
+
+fn maybe_details(
+    ctx: &Context<Model>,
+    title: &str,
+    s: &str,
+    display: bool,
+    toggle: fn(bool) -> Msg,
+) -> Html {
+    let link = ctx.link();
+    if display {
+        let msg = toggle(false);
+        html! {
+            <details open=true
+                     ontoggle={link.callback(move |e: Event| {
+                         let details = e.target_unchecked_into::<HtmlDetailsElement>();
+                         if !details.open() {
+                             msg.clone()
+                         } else {
+                             Msg::NoOp
+                         }
+                     })}>
+
+              <summary><h3 style="display: inline;">{title}</h3></summary>
+              <pre>{s}</pre>
+            </details>
+        }
+    } else {
+        let msg = toggle(true);
+        html! {
+            <details ontoggle={link.callback(move |e: Event| {
+                         let details = e.target_unchecked_into::<HtmlDetailsElement>();
+                         if details.open() {
+                             msg.clone()
+                         } else {
+                             Msg::NoOp
+                         }
+                     })}>
+              <summary><h3 style="display: inline;">{title}</h3></summary>
+            </details>
+        }
+    }
+}
+
+impl Output {
+    fn display(self, ctx: &Context<Model>, model: &Model) -> Html {
+        html! {
+            <>
+            {maybe_show("Errors:", &self.errors)}
+            {maybe_show("Warnings:", &self.warnings)}
+            {maybe_show("Output:", &self.output)}
+            {maybe_show("Bytecode:", &String::from_utf8_lossy(&self.bytecode))}
+            {maybe_details(ctx, "Runtime trace:", &String::from_utf8_lossy(&self.trace), model.trace, Msg::ToggleTrace)}
+            {maybe_details(ctx, "Parsed AST:", &self.ast, model.display_ast, Msg::ToggleDisplayAst)}
+            </>
+        }
+    }
+}
+
+fn eval(model: &Model) -> Output {
+    let mut out = Output::default();
+
+    if model.code.is_empty() {
+        return out;
+    }
+
+    let mut eval = tvix_eval::Evaluation::new(&model.code, Some("/nixbolt".into()));
+    let source = eval.source_map();
+
+    let result = {
+        let mut compiler_observer = DisassemblingObserver::new(source.clone(), &mut out.bytecode);
+        eval.compiler_observer = Some(&mut compiler_observer);
+
+        let mut runtime_observer = TracingObserver::new(&mut out.trace);
+        if model.trace {
+            eval.runtime_observer = Some(&mut runtime_observer);
+        }
+
+        eval.evaluate()
+    };
+
+    if model.display_ast {
+        if let Some(ref expr) = result.expr {
+            out.ast = tvix_eval::pretty_print_expr(expr);
+        }
+    }
+
+    out.output = match result.value {
+        Some(val) => val.to_string(),
+        None => "".to_string(),
+    };
+
+    for warning in result.warnings {
+        writeln!(
+            &mut out.warnings,
+            "{}\n",
+            warning.fancy_format_str(&source).trim(),
+        )
+        .unwrap();
+    }
+
+    if !result.errors.is_empty() {
+        for error in &result.errors {
+            writeln!(
+                &mut out.errors,
+                "{}\n",
+                error.fancy_format_str(&source).trim(),
+            )
+            .unwrap();
+        }
+
+        return out;
+    }
+
+    out
+}
+
+fn main() {
+    yew::start_app::<Model>();
+}
diff --git a/corp/website/content-en.md b/corp/website/content-en.md
new file mode 100644
index 000000000000..3f60ddb23689
--- /dev/null
+++ b/corp/website/content-en.md
@@ -0,0 +1,94 @@
+<p class="lang-links" style="text-align: right;">
+    <a href="/ru">ru</a> | <span class="active-lang">en</span>
+</p>
+<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 website of TVL LLC, the corporate face of the [**TVL**][tvl] Community.
+We are a technology company headquartered in Moscow, working with a variety of topics:
+
+* <details><summary><b>Monorepos</b>. Effective ways for an organisation to
+  structure their internal codebase in a single repository, unify tooling across
+  languages, and reduce <a
+  href="https://en.wikipedia.org/wiki/No_Silver_Bullet#Summary">accidental
+  complexity</a> in software development.</summary>
+
+  With experience from companies like Google, Spotify and DeepMind, we help
+  organisations of different sizes to find streamlined software development
+  workflows that reduce mental load and increase code quality.
+
+  We use our own monorepo solutions in our internal software development flows,
+  and all of this is visible in our [public monorepo][depot].
+  </details>
+* <details><summary><b>Nix</b>. We believe that functional and declarative
+  computer systems are a massive and as-of-yet underrated step forward for
+  computing, and that Nix is the most promising solution for this
+  purpose.</summary>
+
+  Nix allows companies to significantly improve in areas such as:
+
+  1. Unification of development and production environments, leading to fewer
+     surprises when deploying an application.
+
+  2. Tailoring their stack to their use-case. Avoid the complexity of running
+     something like Kubernetes while you are scaling up, but *also* avoid the
+     complexity of rewriting your infrastructure stack once you need it.
+
+  3. Unified developer tooling across different programming languages, without
+     the overhead of using something like Bazel.
+
+  Its radically new model can bring many other advantages which depend
+  on the exact use-cases.
+  </details>
+
+* **Software development**. We offer a wide range of software development
+  services. Whether you need existence with existing projects, or want to create
+  a new solution from scratch, we can help. We specialize in helping
+  organizations avoid the trap of building overly complex systems that don't
+  meet their needs.
+
+* **Site Reliability Engineering (SRE)**. We can help with many infrastructure
+  concerns, such as deployment, scaling, monitoring, troubleshooting analysing
+  failure points in existing solutions. We offer this for any Linux-based
+  technology stack.
+
+--------------
+
+We support open-source software development, and prefer to work on our projects
+in the open. Some of our projects are:
+
+* The public TVL [monorepo][depot], the **depot**, is a demonstration of the
+  monorepo tooling we have been working on for the last couple of years.
+
+  It contains many open-source projects, work by lots of international
+  open-source contributors, and all public code of the company.
+
+* [**Tvix**][tvix], a new implementation of Nix that is fully compatible with
+  existing Nix code. Architectural differences between Nix and Tvix allow us to
+  develop tooling that is better tailored to collaborative software development,
+  and to develop domestic, high-quality solutions for CI/CD.
+
+  We run a demonstration of some parts of Tvix online as [tvixbolt].
+
+* [**Nixery**][nixery] is a service that lets users easily build and deploy
+  ad-hoc container images from their software build definitions.
+
+* Out of personal interest, we also develop free tools that help with learning
+  the Russian language, such as [**Предложник**][predlozhnik].
+
+--------------
+
+Reach out to us at **contact@tvl.su** if you are interested in working with us.
+
+TVL originated as an international community of software developers that wanted
+to socialise and collaborate on projects. Many people from all over the world
+contribute to our open-source software projects. Check out the [TVL community
+website][tvl] for more information.
+
+[tvl]: https://tvl.fyi
+[tvix]: https://tvl.fyi/blog/rewriting-nix
+[nixery]: https://nixery.dev
+[predlozhnik]: https://predlozhnik.ru/
+[depot]: https://cs.tvl.fyi/depot
diff --git a/corp/website/content-ru.md b/corp/website/content-ru.md
new file mode 100644
index 000000000000..f61d3c6bf9dc
--- /dev/null
+++ b/corp/website/content-ru.md
@@ -0,0 +1,98 @@
+<p class="lang-links" style="text-align: right;">
+    <span class="active-lang">ru</span> | <a href="/en">en</a>
+</p>
+<img class="tvl-logo" src="https://static.tvl.su/latest/logo-animated.svg"
+     alt="Вирус со спайк-белками в форме лямбды, сидящий на диване">
+
+----------------
+
+Добро пожаловать на официальный сайт ТВЛ. Мы - технологическая компания в
+Москве, занимающаяся следующими направлениями:
+
+* <details><summary><b>Монорепозитории</b>. Набор эффективных способов по
+  структурированию внутренней кодовой базы в едином репозитории, унификации
+  инструментов разработки между различными языками, а также снижения <a
+  href="https://ru.wikipedia.org/wiki/%D0%A1%D0%B5%D1%80%D0%B5%D0%B1%D1%80%D1%8F%D0%BD%D0%BE%D0%B9_%D0%BF%D1%83%D0%BB%D0%B8_%D0%BD%D0%B5%D1%82#%D0%9E%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5">побочныx
+  сложностей</a> в разработке ПО.</summary>
+
+  Наш опыт работы с такими компаниями, как Google, Spotify и DeepMind, позволяет
+  нам помочь организациям любого размера оптимизировать процессы разработки ПО,
+  уменьшить нагрузку на разработчиков и улучшить качество кода.
+
+  Мы используем собственные монорепо-решения во внутренних процессах разработки
+  ПО. Всё это можно увидеть в нашем [публичном монорепозитории][depot].
+  </details>
+* <details><summary><b>Nix</b>. Мы считаем, что функциональные и декларативные
+  компьютерные системы являются важным и пока ещё недооцененным подходом к
+  вычислениям, и <a href="https://nixos.org">Nix</a> является наиболее
+  перспективным решением в этом направлении.</summary>
+
+  Nix позволяет компаниям значительно улучшить следующие области:
+
+  1. Унификация сред разработки и производства, что приводит к меньшему
+     количеству сюрпризов при развертывании приложения.
+
+  2. Настройка стека компании под конкретные задачи. Избегайте сложности
+     инструментов вроде Kubernetes при масштабировании, но также избегайте
+     изменения всей инфраструктуры по малейшей необходимости.
+
+  3. Единый набор инструментов разработки для различных языков программирования,
+     без сложностей, связанных с использованием Bazel и подобных.
+
+  Nix прелагает радикально новую модель, которая может принести множество
+  преимуществ для каждого способа использования.
+  </details>
+
+* **Разработка программного обеспечения**. Мы предлагаем широкий спектр услуг по
+  разработке ПО. Вне зависимости от того, нуждаетесь вы в поддержке существующих
+  проектов или хотите создать новое решение с нуля, мы можем вам помочь. Наша
+  задача - помочь компаниям избежать ловушки излишне сложных систем, которые не
+  соответствуют их потребностям.
+
+* **Site Reliability Engineering (SRE)**. Мы можем помочь с множеством
+  инфраструктурных проблем, таких как развертывание, масштабирование,
+  мониторинг, анализ и устранение неполадок в существующих решениях. Мы
+  работаем с любыми технологическими стеками на базе Linux.
+
+--------------
+
+Мы поддерживаем разработку программного обеспечения с открытым исходным кодом и
+предпочитаем работать над нашими проектами в открытом доступе. Некоторые из
+наших проектов:
+
+* Публичный репозиторий ТВЛ, [**depot**][depot], является демонстрацией
+  инструментов монорепо, над которыми мы работаем в течение последних нескольких
+  лет.
+
+  В нем содержится множество проектов c открытым исходным кодом, результат
+  работы разработчиков из разных стран, а также весь публичный код компании.
+
+* [**Tvix**][tvix] - это новая реализация Nix, которая полностью совместима с
+  существующим кодом Nix. Архитектурные различия между Nix и Tvix позволяют нам
+  разрабатывать инструменты, которые лучше подходят для совместной разработки
+  программного обеспечения, и разрабатывать отечественные качественные решения
+  для CI/CD.
+
+  Мы запускаем демонстрацию некоторых компонентов Tvix онлайн: [tvixbolt][].
+
+* [Nixery][nixery] - это сервис, который позволяет пользователям с легкостью
+  создавать и развёртывать образы контейнеров напрямую из их инструкций сборки
+  софта.
+
+* Из личных интересов, мы также разрабатываем бесплатные инструменты, которые
+  помогают в изучении русского языка, такие как [**Предложник**][predlozhnik].
+
+Свяжитесь с нами по адресу **contact@tvl.su**, если вы заинтересованы в
+сотрудничестве с нами.
+
+ТВЛ возникла как международное сообщество программистов для общения и совместной
+работы над проектами. Люди из разных стран вносят свой вклад в наши открытые
+проекты по разработке программного обеспечения. Посетите [веб-сайт сообщества
+TVL][tvl], чтобы узнать больше.
+
+[tvl]: https://tvl.fyi
+[tvix]: https://tvl.fyi/blog/rewriting-nix
+[nixery]: https://nixery.dev
+[predlozhnik]: https://predlozhnik.ru/
+[depot]: https://cs.tvl.fyi/depot
+[tvixbolt]: https://tvixbolt.tvl.su/
diff --git a/corp/website/default.nix b/corp/website/default.nix
new file mode 100644
index 000000000000..a8ac132cb217
--- /dev/null
+++ b/corp/website/default.nix
@@ -0,0 +1,59 @@
+{ 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/latest/logo-animated.svg";
+  };
+
+  common = description: {
+    extraFooter = "\n|\n © ООО ТВЛ";
+    staticUrl = "https://static.tvl.su/latest";
+
+    extraHead = ''
+      <meta name="description" content="${description}">
+      <script type="application/ld+json">
+        ${builtins.toJSON structuredData}
+      </script>
+      <style>
+        .tvl-logo {
+          width: 60%;
+          display: block;
+          margin-left: auto;
+          margin-right: auto;
+        }
+
+        .active-lang {
+          color: black;
+          font-weight: bold;
+        }
+
+        .inactive-lang {
+          color: inherit;
+        }
+      </style>
+    '';
+  };
+
+  descEn = "TVL provides technology consulting for monorepos, Nix, and other SRE/DevOps/Software Engineering topics.";
+  indexEn = depot.web.tvl.template ({
+    title = "TVL (The Virus Lounge) - Software consulting";
+    content = builtins.readFile ./content-en.md;
+  } // common descEn);
+
+  descRu = "TVL предоставляет технологическое консультирование по монорепозиториям, Nix и другим темам SRE/DevOps/Software Engineering.";
+  indexRu = depot.web.tvl.template ({
+    title = "ТВЛ - Монорепозитории, SRE, Nix, программное обеспечение";
+    content = builtins.readFile ./content-ru.md;
+  } // common descRu);
+in
+pkgs.runCommand "corp-website" { } ''
+  mkdir -p $out/{en,ru}
+  cp ${indexEn} $out/index.html
+  cp ${indexEn} $out/en/index.html
+  cp ${indexRu} $out/ru/index.html
+''
diff --git a/default.nix b/default.nix
new file mode 100644
index 000000000000..832cc2448105
--- /dev/null
+++ b/default.nix
@@ -0,0 +1,123 @@
+# 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 ? { }
+, localSystem ? builtins.currentSystem
+, ...
+}@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!";
+      builtins = builtins // {
+        currentSystem = throw "Use localSystem from the readTree args instead of builtins.currentSystem!";
+      };
+    };
+  };
+
+  # 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 {
+  inherit localSystem;
+  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; };
+  });
+
+  # Derivation that gcroots all depot targets.
+  ci.gcroot = with self.third_party.nixpkgs; writeText "depot-gcroot"
+    (builtins.concatStringsSep "\n"
+      (lib.flatten
+        (map (p: map (o: p.${o}) p.outputs or [ ]) # list all outputs of each drv
+          self.ci.targets)));
+})
diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000000..0e46bbedb0cd
--- /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 000000000000..f13125c44935
--- /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 via mail at
+[depot@tvl.su](mailto:depot@tvl.su) or on IRC.
+
+## 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 000000000000..7f72d81d9a9f
--- /dev/null
+++ b/docs/REVIEWS.md
@@ -0,0 +1,191 @@
+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)
+    - [Gerrit for Github users](#gerrit-for-github-users)
+
+<!-- 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 -pOP 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@tvl.su` 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 inbox][].
+
+## Gerrit for Github Users
+
+There is a walkthrough that describes [only the parts that differ
+from Github][github-diff], although it does not cover [attention
+sets][], which are important to understand.
+
+### Attention Sets
+
+The attention set of a CL is somewhat similar to the set of Github
+users who have unread notifications for a PR.  The "your turn" list
+on the dashboard is similar to your unread notifications list in
+Github.  These similarities are only rough approximations, however.
+
+Unfortunately the rules for updating attention sets are very
+different and complex.  If you don't read and understand them, you
+may end up leaving comments that nobody ever finds out about.  Here
+are a few unexpected features:
+
+- Voting on or replying to a CL will remove you from the attention
+  set.  You can also do this by clicking on the gray chevron shape
+  next to your name.
+
+- If you comment on a merged or abandoned change without marking
+  your comment "unresolved", *nobody will be notified of your
+  comment*.  If you want to the owner of a merged or abandoned
+  change to read your comment, you must mark it as "unresolved" or
+  manually add them to the attention set by hovering your mouse over
+  their name and clicking "add to attention set"
+
+There are many more [rules][attention-set-rules], which you should
+read.
+
+
+[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 inbox]: https://inbox.tvl.su/depot/
+[hackint]: https://hackint.org
+[github-diff]: https://gerrit.wikimedia.org/r/Documentation/intro-gerrit-walkthrough-github.html
+[attention sets]: https://gerrit-review.googlesource.com/Documentation/user-attention-set.html
+[attention-set-rules]: https://gerrit-review.googlesource.com/Documentation/user-attention-set.html#_rules
diff --git a/docs/designs/SPARSE_CHECKOUTS.md b/docs/designs/SPARSE_CHECKOUTS.md
new file mode 100644
index 000000000000..820cb2c5861c
--- /dev/null
+++ b/docs/designs/SPARSE_CHECKOUTS.md
@@ -0,0 +1,43 @@
+NOTE: This proposal is archived. We run `josh` instead, and long-term
+might want to integrate per-target dependency analysis with josh's
+workspace functionality.
+
+-------------
+
+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 000000000000..1b1ea5f9d012
--- /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 000000000000..78585da236e3
--- /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 000000000000..e5a5990ef25a
--- /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 000000000000..e6b9c2fb9bf9
--- /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 000000000000..3b6ce0a7392f
--- /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 000000000000..024451858bbc
--- /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 000000000000..c02b30f76ea8
--- /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 000000000000..7fbfaa3f5c47
--- /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 000000000000..75987a2da601
--- /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 000000000000..d45876129b3a
--- /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 000000000000..ae69b2fc4c18
--- /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 000000000000..f1e366ba9d21
--- /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 000000000000..d5434526e875
--- /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 000000000000..6aa106405a4b
--- /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 000000000000..0fcd60373ff7
--- /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 000000000000..160158d177b8
--- /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 000000000000..f8b8a014131f
--- /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 000000000000..0c99d00c82df
--- /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 000000000000..fa659de8910e
--- /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 000000000000..c531d9867f6c
--- /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 000000000000..994b1138ef8f
--- /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 000000000000..0d021f78e89b
--- /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 000000000000..102aa3791481
--- /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 000000000000..16a26218a0c0
--- /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 000000000000..be74b9207e72
--- /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 000000000000..c4013ac13ebc
--- /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 000000000000..94a9ed024d38
--- /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 000000000000..064742c0091f
--- /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 000000000000..54f8e5f34462
--- /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 000000000000..339b86d26941
--- /dev/null
+++ b/fun/gemma/default.nix
@@ -0,0 +1,56 @@
+{ depot, ... }:
+
+let
+  inherit (depot.third_party.elmPackages_0_18) cacert iana-etc libredirect stdenv runCommand writeText elmPackages;
+
+  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* "${runCommand "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 000000000000..e449908e499c
--- /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 000000000000..2ae541ae0b95
--- /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 000000000000..20a76caae629
--- /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 000000000000..922047617f25
--- /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 000000000000..1acf287bfbd7
--- /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 000000000000..35c3bbae9039
--- /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 000000000000..10a85eba0af0
--- /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 000000000000..f10c3b86f24e
--- /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 000000000000..051d09d292a8
--- /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 000000000000..8e850e7a0af2
--- /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 000000000000..c2a6179bfbbf
--- /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 000000000000..04f98e97fba1
--- /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 000000000000..16a6c1526259
--- /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 000000000000..ef5477ea1bf2
--- /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 000000000000..c812e5e11633
--- /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 000000000000..6b2c34654382
--- /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 000000000000..0304581d9d4a
--- /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 000000000000..3bf5e51dba2f
--- /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 000000000000..8cb01f2e3d35
--- /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 000000000000..2340612c2eb2
--- /dev/null
+++ b/fun/paroxysm/Cargo.lock
@@ -0,0 +1,1733 @@
+# 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 = "android_system_properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
+dependencies = [
+ "libc",
+]
+
+[[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.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+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 = "bumpalo"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+
+[[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.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits 0.2.15",
+ "time",
+ "wasm-bindgen",
+ "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.144",
+ "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.144",
+ "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.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.56+curl-7.83.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6093e169dd4de29e468fa649fbae11cdcd5551c81fe5bf1b0677adad7ef3d26f"
+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.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+
+[[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 = "iana-time-zone"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi 0.3.9",
+]
+
+[[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.144",
+ "serde_derive",
+ "tokio-codec",
+ "tokio-core",
+ "tokio-io",
+ "tokio-mockstream",
+ "tokio-timer 0.1.2",
+ "tokio-tls",
+ "toml",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "js-sys"
+version = "0.3.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[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.132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc"
+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.7",
+ "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.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
+
+[[package]]
+name = "openssl"
+version = "0.10.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0"
+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.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
+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.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api 0.4.7",
+ "parking_lot_core 0.9.3",
+]
+
+[[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.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec 1.9.0",
+ "windows-sys",
+]
+
+[[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.144",
+]
+
+[[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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b845d6d8ec554f972a2c5298aad68953fd64e7441e846075450b44656a016d1"
+dependencies = [
+ "vcpkg",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+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.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r2d2"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
+dependencies = [
+ "log",
+ "parking_lot 0.12.1",
+ "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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf"
+dependencies = [
+ "parking_lot 0.12.1",
+]
+
+[[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.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
+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.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+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.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde 1.0.144",
+]
+
+[[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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[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.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
+
+[[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.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+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.16",
+ "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.7",
+ "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.7",
+ "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.7",
+ "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.144",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+
+[[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 = "wasm-bindgen"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
+
+[[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.6",
+]
diff --git a/fun/paroxysm/Cargo.toml b/fun/paroxysm/Cargo.toml
new file mode 100644
index 000000000000..4d282285fd97
--- /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 000000000000..1120d0dcc8db
--- /dev/null
+++ b/fun/paroxysm/OWNERS
@@ -0,0 +1 @@
+eta
diff --git a/fun/paroxysm/README.md b/fun/paroxysm/README.md
new file mode 100644
index 000000000000..a595530982da
--- /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 000000000000..1aeaeb9912eb
--- /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
+    pkg-config
+    postgresql.lib
+    curl
+  ];
+}
diff --git a/fun/paroxysm/docker/default.nix b/fun/paroxysm/docker/default.nix
new file mode 100644
index 000000000000..cb5b2758ec63
--- /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 000000000000..aa02f4f63f92
--- /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 000000000000..e8b52d5a9b91
--- /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 000000000000..291a97c5ce1f
--- /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 000000000000..4885ae5edefe
--- /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 000000000000..cfb2e2073e27
--- /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 000000000000..fa40f5347a75
--- /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 000000000000..46a215c22563
--- /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 000000000000..721efbbb2e61
--- /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 000000000000..ef4044531ee7
--- /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 000000000000..622119552e47
--- /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 000000000000..de197a219ebf
--- /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 000000000000..173fa9efc390
--- /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 000000000000..b12c5e6f0c29
--- /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 000000000000..3daeac34d6bb
--- /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 000000000000..aa2e318262cd
--- /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 000000000000..0db2fe300e1e
--- /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 000000000000..50b47418d1a8
--- /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 000000000000..498cbac2f2ab
--- /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 000000000000..a58e6540a3f3
--- /dev/null
+++ b/fun/tvl-ebooks/OWNERS
@@ -0,0 +1 @@
+ben
diff --git a/fun/tvl-ebooks/default.nix b/fun/tvl-ebooks/default.nix
new file mode 100644
index 000000000000..68e61c2525b8
--- /dev/null
+++ b/fun/tvl-ebooks/default.nix
@@ -0,0 +1,7 @@
+{ pkgs, ... }:
+
+pkgs.buildGoModule {
+  name = "tvl-ebooks";
+  vendorHash = "sha256: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 000000000000..7954184dd7be
--- /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 000000000000..154fbf8216ef
--- /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 000000000000..bd3ff8761342
--- /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 000000000000..bbd607405cdf
--- /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 000000000000..6d4d0047bbac
--- /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 000000000000..ba4863414029
--- /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 000000000000..a2d0d20eeb9c
--- /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 000000000000..980ad16bcc9d
--- /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 000000000000..94f8cf092507
--- /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 000000000000..c4eeff7f5897
--- /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 000000000000..bfb8e1952f7e
--- /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 000000000000..712c96cd95d4
--- /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 000000000000..dc32e4d1cefb
--- /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 000000000000..a7ab65d3d196
--- /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 000000000000..1af52d0cd4eb
--- /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 000000000000..37f65e0faef4
--- /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 000000000000..e4d9804c807c
--- /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 000000000000..83c7e8c4f1cc
--- /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 000000000000..2e9580706346
--- /dev/null
+++ b/fun/🕰️/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/fun/🕰️/bin.lisp b/fun/🕰️/bin.lisp
new file mode 100644
index 000000000000..1f5a2b2e0518
--- /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 000000000000..2b1a94640009
--- /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 000000000000..d23db033743e
--- /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 000000000000..c95d2e9a687e
--- /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 000000000000..cee7bceb54a0
--- /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 000000000000..cb2445b460c9
--- /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 000000000000..46243ac8d3f0
--- /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 000000000000..2b8bfaa8bcc4
--- /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 000000000000..d2800bc9413a
--- /dev/null
+++ b/lisp/klatre/OWNERS
@@ -0,0 +1 @@
+grfn
diff --git a/lisp/klatre/default.nix b/lisp/klatre/default.nix
new file mode 100644
index 000000000000..2c7bb6490a9f
--- /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 000000000000..79c7259752c6
--- /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 000000000000..41174bbb3cf7
--- /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 000000000000..143b1ca01474
--- /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 000000000000..9986cda17469
--- /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 000000000000..b5135d14d10c
--- /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 000000000000..94a9ed024d38
--- /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 000000000000..c6da0ab8da89
--- /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 000000000000..aa1df10c8bd9
--- /dev/null
+++ b/net/alcoholic_jwt/default.nix
@@ -0,0 +1,9 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  buildInputs = with pkgs; [
+    openssl
+    pkg-config
+  ];
+}
diff --git a/net/alcoholic_jwt/src/lib.rs b/net/alcoholic_jwt/src/lib.rs
new file mode 100644
index 000000000000..4a3ece8af170
--- /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 000000000000..c264bb5a13a1
--- /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 000000000000..693699042b1a
--- /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 000000000000..05f33e85b800
--- /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 000000000000..1e529cfadc73
--- /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 000000000000..94a9ed024d38
--- /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 000000000000..65a9d4293b79
--- /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 000000000000..aa1df10c8bd9
--- /dev/null
+++ b/net/crimp/default.nix
@@ -0,0 +1,9 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  buildInputs = with pkgs; [
+    openssl
+    pkg-config
+  ];
+}
diff --git a/net/crimp/src/lib.rs b/net/crimp/src/lib.rs
new file mode 100644
index 000000000000..4dd4d6c31bd7
--- /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 000000000000..e8e9223ce804
--- /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 000000000000..8e46d5a07f8f
--- /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 000000000000..b3bc54673d0b
--- /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 000000000000..21c95a11a2e9
--- /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 000000000000..30c933b56302
--- /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 000000000000..baf0e271d1f0
--- /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 000000000000..2ba3e69f992a
--- /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 000000000000..3a298bc9bf8b
--- /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 000000000000..80981d37ab52
--- /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 000000000000..a64022791462
--- /dev/null
+++ b/nix/OWNERS
@@ -0,0 +1 @@
+Profpatsch
diff --git a/nix/binify/default.nix b/nix/binify/default.nix
new file mode 100644
index 000000000000..a9900caf43d5
--- /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 000000000000..f247e6839eee
--- /dev/null
+++ b/nix/bufCheck/default.nix
@@ -0,0 +1,22 @@
+# Check protobuf syntax and breaking.
+#
+{ depot, pkgs, ... }:
+
+pkgs.writeShellScriptBin "ci-buf-check" ''
+  export PATH="$PATH:${pkgs.lib.makeBinPath [ pkgs.buf pkgs.protoc-gen-go pkgs.protoc-gen-go-grpc ]}"
+  (cd $(git rev-parse --show-toplevel) && buf lint .)
+
+  # Run buf generate, and bail out if generated files are changed.
+  (cd $(git rev-parse --show-toplevel) && buf generate --path tvix/store/protos)
+  # Check if any files have changed
+  if [[ -n "$(git status --porcelain -unormal)" ]]; then
+      echo "-----------------------------"
+      echo ".pb.go files need to be updated"
+      echo "-----------------------------"
+      git status -unormal
+      exit 1
+  fi
+
+  # Report-only
+  (cd $(git rev-parse --show-toplevel) && (buf breaking . --against "./.git#ref=HEAD~1" || true))
+''
diff --git a/nix/buildGo/.skip-subtree b/nix/buildGo/.skip-subtree
new file mode 100644
index 000000000000..8db1f814f653
--- /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 000000000000..e9667c039ab7
--- /dev/null
+++ b/nix/buildGo/README.md
@@ -0,0 +1,117 @@
+buildGo.nix
+===========
+
+This is an alternative [Nix][] build system for [Go][]. It supports building Go
+libraries and programs.
+
+*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
+├── main.go      <-- program implementation
+└── default.nix  <-- build instructions
+```
+
+The contents of `default.nix` could look like this:
+
+```nix
+{ buildGo }:
+
+let
+  lib = buildGo.package {
+    name = "somelib";
+    srcs = [
+      ./lib/bar.go
+      ./lib/foo.go
+    ];
+  };
+in buildGo.program {
+  name = "my-program";
+  deps = [ 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        |
+
+## 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 000000000000..bd2e32330060
--- /dev/null
+++ b/nix/buildGo/default.nix
@@ -0,0 +1,121 @@
+# 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 runCommand fetchFromGitHub protobuf symlinkJoin;
+
+  # TODO: Adapt to Go 1.19 changes
+  go = pkgs.go_1_18;
+
+  # 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; };
+
+in
+{
+  # Only the high-level builder functions are exposed, but made
+  # overrideable.
+  program = makeOverridable program;
+  package = makeOverridable package;
+  external = makeOverridable external;
+}
diff --git a/nix/buildGo/example/default.nix b/nix/buildGo/example/default.nix
new file mode 100644
index 000000000000..6756bf39e20b
--- /dev/null
+++ b/nix/buildGo/example/default.nix
@@ -0,0 +1,40 @@
+# 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.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
+  ];
+
+  x_defs = {
+    "main.Flag" = "successfully";
+  };
+}
diff --git a/nix/buildGo/example/lib.go b/nix/buildGo/example/lib.go
new file mode 100644
index 000000000000..8a61370e994c
--- /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 000000000000..bbcedbff8726
--- /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/external/default.nix b/nix/buildGo/external/default.nix
new file mode 100644
index 000000000000..42592c67e482
--- /dev/null
+++ b/nix/buildGo/external/default.nix
@@ -0,0 +1,112 @@
+# 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
+    unsafeDiscardStringContext
+    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
+  '';
+  # readFile adds the references of the read in file to the string context for
+  # Nix >= 2.6 which would break the attribute set construction in fromJSON
+  analysis = fromJSON (unsafeDiscardStringContext (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 000000000000..a77c43b371e9
--- /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/buildLisp/README.md b/nix/buildLisp/README.md
new file mode 100644
index 000000000000..0d1e46983422
--- /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 000000000000..0d68a2818b7d
--- /dev/null
+++ b/nix/buildLisp/default.nix
@@ -0,0 +1,778 @@
+# 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 runCommand 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 [ ];
+            targets = lib.subtractLists (brokenOn ++ [ 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
+    runCommand 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: runCommand "${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.isPower && targetPlatform.is32bit then "pfsl"
+        else if targetPlatform.isPower && 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: runCommand "${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: runCommand "${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 000000000000..6add2676f10c
--- /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 000000000000..e557de4ae5fd
--- /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 000000000000..a29390cf4dba
--- /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 000000000000..ca5f2b9741e6
--- /dev/null
+++ b/nix/buildLisp/tests/argv0.nix
@@ -0,0 +1,58 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  # Trivial test program that outputs argv[0] and exits
+  prog =
+    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";
+        }
+      ];
+    };
+
+  # Extract verify argv[0] output for given buildLisp program
+  checkImplementation = prog:
+    pkgs.runCommand "check-argv0" { } ''
+      set -eux
+
+      checkInvocation() {
+        invocation="$1"
+        test "$invocation" = "$("$invocation")"
+      }
+
+      checkInvocation "${prog}/bin/argv0-test"
+
+      cd ${prog}
+      checkInvocation "./bin/argv0-test"
+
+      cd bin
+      checkInvocation ./argv0-test
+
+      set +x
+
+      touch "$out"
+    '';
+
+  inherit (prog.meta.ci) targets;
+in
+
+(checkImplementation prog).overrideAttrs (_: {
+  # Wire up a subtarget all (active) non-default implementations
+  passthru = lib.genAttrs targets (name: checkImplementation prog.${name});
+
+  meta.ci = { inherit targets; };
+})
diff --git a/nix/buildManPages/OWNERS b/nix/buildManPages/OWNERS
new file mode 100644
index 000000000000..2e9580706346
--- /dev/null
+++ b/nix/buildManPages/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/nix/buildManPages/default.nix b/nix/buildManPages/default.nix
new file mode 100644
index 000000000000..746ed25182b4
--- /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 000000000000..4849d1b8ea54
--- /dev/null
+++ b/nix/buildkite/default.nix
@@ -0,0 +1,451 @@
+# 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
+    concatLists
+    concatStringsSep
+    elem
+    foldl'
+    hasAttr
+    hashString
+    isNull
+    isString
+    length
+    listToAttrs
+    mapAttrs
+    toJSON
+    unsafeDiscardStringContext;
+
+  inherit (pkgs) lib runCommand 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: cancelOnBuildFailing:
+    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;
+      cancel_on_build_failing = cancelOnBuildFailing;
+
+      # 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 192 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 ? [ ]
+      # The list of phases known by the current Buildkite
+      # pipeline. Dynamic pipeline chunks for each phase are uploaded
+      # to Buildkite on execution of static part of the
+      # pipeline. Phases selection is hard-coded in the static
+      # pipeline.
+      #
+      # Pipeline generation will fail when an extra step with
+      # unregistered phase is added.
+      #
+      # Common scenarios for different phase:
+      #   - "build" - main phase for building all Nix targets
+      #   - "release" - pushing artifacts to external repositories
+      #   - "deploy" - updating external deployment configurations
+    , phases ? [ "build" "release" ]
+      # Build phases that are active for this invocation (i.e. their
+      # steps should be generated).
+      #
+      # This can be used to disable outputting parts of a pipeline if,
+      # for example, build and release phases are created in separate
+      # eval contexts.
+      #
+      # TODO(tazjin): Fail/warn if unknown phase is requested.
+    , activePhases ? phases
+      # Setting this attribute to true cancels dynamic pipeline steps
+      # as soon as the build is marked as failing.
+      #
+      # To enable this feature one should enable "Fail Fast" setting
+      # at Buildkite pipeline or on organization level.
+    , cancelOnBuildFailing ? false
+    }:
+    let
+      # List of phases to include.
+      enabledPhases = lib.intersectLists activePhases phases;
+
+      # Is the 'build' phase included? This phase is treated specially
+      # because it always contains the plain Nix builds, and some
+      # logic/optimisation depends on knowing whether is executing.
+      buildEnabled = elem "build" enabledPhases;
+
+      # Convert a target into all of its steps, separated by build
+      # phase (as phases end up in different chunks).
+      targetToSteps = target:
+        let
+          step = mkStep headBranch parentTargetMap target cancelOnBuildFailing;
+
+          # 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) cancelOnBuildFailing;
+
+          # Split extra steps by phase.
+          splitExtraSteps = lib.groupBy ({ phase, ... }: phase)
+            (attrValues (mapAttrs (normaliseExtraStep phases overridable)
+              (target.meta.ci.extraSteps or { })));
+
+          extraSteps = mapAttrs
+            (_: steps:
+              map (mkExtraStep buildEnabled) steps)
+            splitExtraSteps;
+        in
+        if !buildEnabled then extraSteps
+        else extraSteps // {
+          build = [ step ] ++ (extraSteps.build or [ ]);
+        };
+
+      # Combine all target steps into step lists per phase.
+      #
+      # TODO(tazjin): Refactor when configurable phases show up.
+      globalSteps = {
+        build = additionalSteps;
+        release = postBuildSteps;
+      };
+
+      phasesWithSteps = lib.zipAttrsWithNames enabledPhases (_: concatLists)
+        ((map targetToSteps drvTargets) ++ [ globalSteps ]);
+
+      # Generate pipeline chunks for each phase.
+      chunks = foldl'
+        (acc: phase:
+          let phaseSteps = phasesWithSteps.${phase} or [ ]; in
+          if phaseSteps == [ ]
+          then acc
+          else acc ++ (pipelineChunks phase phaseSteps))
+        [ ]
+        enabledPhases;
+
+    in
+    runCommand "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 prompt;
+        branches = step.branches or [ ];
+        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" ])
+    ];
+  };
+
+  # Validate and normalise extra step configuration before actually
+  # generating build steps, in order to use user-provided metadata
+  # during the pipeline generation.
+  normaliseExtraStep = phases: overridableParent: key:
+    { command
+    , label ? key
+    , needsOutput ? false
+    , parentOverride ? (x: x)
+    , branches ? null
+    , alwaysRun ? false
+    , prompt ? false
+    , softFail ? false
+
+      # TODO(tazjin): Default to 'build' after 2022-10-01.
+    , phase ? if (isNull postBuild || !postBuild) then "build" else "release"
+
+      # TODO(tazjin): Turn into hard-failure after 2022-10-01.
+    , postBuild ? null
+    , skip ? false
+    , agents ? null
+    }:
+    let
+      parent = overridableParent parentOverride;
+      parentLabel = parent.env.READTREE_TARGET;
+
+      validPhase = lib.throwIfNot (elem phase phases) ''
+        In step '${label}' (from ${parentLabel}):
+
+        Phase '${phase}' is not valid.
+
+        Known phases: ${concatStringsSep ", " phases}
+      ''
+        phase;
+    in
+    {
+      inherit
+        alwaysRun
+        branches
+        command
+        key
+        label
+        needsOutput
+        parent
+        parentLabel
+        softFail
+        skip
+        agents;
+
+      # //nix/buildkite is growing a new feature for adding different
+      # "build phases" which supersedes the previous `postBuild`
+      # boolean API.
+      #
+      # To help users transition, emit warnings if the old API is used.
+      phase = lib.warnIfNot (isNull postBuild) ''
+        In step '${label}' (from ${parentLabel}):
+
+        Please note: The CI system is introducing support for running
+        steps in different build phases.
+
+        The currently supported phases are 'build' (all Nix targets,
+        extra steps such as tests that feed into the build results,
+        etc.) and 'release' (steps that run after builds and tests
+        have already completed).
+
+        This replaces the previous boolean `postBuild` API in extra
+        step definitions. Please remove the `postBuild` parameter from
+        this step and instead set `phase = "${phase}";`.
+      ''
+        validPhase;
+
+      prompt = lib.throwIf (prompt != false && phase == "build") ''
+        In step '${label}' (from ${parentLabel}):
+
+        The 'prompt' feature can only be used by steps in the "release"
+        phase, because CI builds should not be gated on manual human
+        approvals.
+      ''
+        prompt;
+    };
+
+  # Create the Buildkite configuration for an extra step, optionally
+  # wrapping it in a gate group.
+  mkExtraStep = buildEnabled: cfg:
+    let
+      step = {
+        key = hashString "sha1" "${cfg.label}-${cfg.parentLabel}";
+        label = ":gear: ${cfg.label} (from ${cfg.parentLabel})";
+        skip =
+          let
+            # When parent doesn't have skip attribute set, default to false
+            parentSkip = cfg.parent.skip or false;
+            # Extra step skip parameter can be string explaining the
+            # skip reason.
+            extraStepSkip = if builtins.isString cfg.skip then true else cfg.skip;
+            # Don't run if extra step is explicitly set to skip. If
+            # parameter is not set or equal to false, follow parent behavior.
+            skip' = if extraStepSkip then cfg.skip else parentSkip;
+          in
+          if cfg.alwaysRun then false else skip';
+
+        depends_on = lib.optional
+          (buildEnabled && !cfg.alwaysRun && !cfg.needsOutput)
+          cfg.parent.key;
+
+        command = pkgs.writeShellScript "${cfg.key}-script" ''
+          set -ueo pipefail
+          ${lib.optionalString cfg.needsOutput
+            "echo '~~~ Preparing build output of ${cfg.parentLabel}'"
+          }
+          ${lib.optionalString cfg.needsOutput cfg.parent.command}
+          echo '+++ Running extra step command'
+          exec ${cfg.command}
+        '';
+
+        soft_fail = cfg.softFail;
+      } // (lib.optionalAttrs (cfg.agents != null) { inherit (cfg) agents; })
+      // (lib.optionalAttrs (cfg.branches != null) {
+        branches = lib.concatStringsSep " " cfg.branches;
+      });
+    in
+    if (isString cfg.prompt)
+    then
+      mkGatedStep
+        {
+          inherit step;
+          inherit (cfg) 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 000000000000..8afac1e5ec04
--- /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/dependency-analyzer/default.nix b/nix/dependency-analyzer/default.nix
new file mode 100644
index 000000000000..54ff72912ea8
--- /dev/null
+++ b/nix/dependency-analyzer/default.nix
@@ -0,0 +1,258 @@
+{ lib, depot, pkgs, ... }:
+
+let
+  inherit (builtins) unsafeDiscardStringContext appendContext;
+
+  #
+  # Utilities
+  #
+
+  # Determine all paths a derivation depends on, i.e. input derivations and
+  # files imported into the Nix store.
+  #
+  # Implementation for Nix < 2.6 is quite hacky at the moment.
+  #
+  # Type: str -> [str]
+  #
+  # TODO(sterni): clean this up and expose it
+  directDrvDeps =
+    if lib.versionAtLeast builtins.nixVersion "2.6"
+    then
+    # Since https://github.com/NixOS/nix/pull/1643, Nix apparently »preserves
+    # string context« through a readFile invocation. This has the side effect
+    # that it becomes possible to query the actual references a store path has.
+    # Not a 100% sure this is intended, but _very_ convenient for us here.
+      drvPath:
+      # if the passed path is not a derivation we can't necessarily get its
+      # dependencies, since it may not be representable as a Nix string due to
+      # NUL bytes, e.g. compressed patch files imported into the Nix store.
+      if builtins.match "^.+\\.drv$" drvPath == null
+      then [ ]
+      else builtins.attrNames (builtins.getContext (builtins.readFile drvPath))
+    else
+    # For Nix < 2.6 we have to rely on HACK, namely grepping for quoted store
+    # path references in the file. In the future this should be replaced by
+    # a proper derivation parser.
+      drvPath: builtins.concatLists (
+        builtins.filter builtins.isList (
+          builtins.split
+            "\"(${lib.escapeRegex builtins.storeDir}/[[:alnum:]+._?=-]+.drv)\""
+            (builtins.readFile drvPath)
+        )
+      );
+
+  # Maps a list of derivation to the list of corresponding `drvPath`s.
+  #
+  # Type: [drv] -> [str]
+  drvsToPaths = drvs:
+    builtins.map (drv: builtins.unsafeDiscardOutputDependency drv.drvPath) drvs;
+
+  #
+  # Calculate map of direct derivation dependencies
+  #
+
+  # Create the dependency map entry for a given `drvPath` which mainly includes
+  # a list of other `drvPath`s it depends on. Additionally we store whether the
+  # derivation is `known`, i.e. part of the initial list of derivations we start
+  # generating the map from
+  #
+  # Type: bool -> string -> set
+  drvEntry = known: drvPath:
+    let
+      # key may not refer to a store path, …
+      key = unsafeDiscardStringContext drvPath;
+      # but we must read from the .drv file.
+      path = builtins.unsafeDiscardOutputDependency drvPath;
+    in
+    {
+      inherit key;
+      # trick so we can call listToAttrs directly on the result of genericClosure
+      name = key;
+      value = {
+        deps = directDrvDeps path;
+        inherit known;
+      };
+    };
+
+  # Create an attribute set that maps every derivation in the combined
+  # dependency closure of the list of input derivation paths to every of their
+  # direct dependencies. Additionally every entry will have set their `known`
+  # attribute to `true` if it is in the list of input derivation paths.
+  #
+  # Type: [str] -> set
+  plainDrvDepMap = drvPaths:
+    builtins.listToAttrs (
+      builtins.genericClosure {
+        startSet = builtins.map (drvEntry true) drvPaths;
+        operator = { value, ... }: builtins.map (drvEntry false) value.deps;
+      }
+    );
+
+  #
+  # Calculate closest known dependencies in the dependency map
+  #
+
+  inherit (depot.nix.stateMonad)
+    after
+    bind
+    for_
+    get
+    getAttr
+    run
+    setAttr
+    pure
+    ;
+
+  # This is an action in stateMonad which expects the (initial) state to have
+  # been produced by `plainDrvDepMap`. Given a `drvPath`, it calculates a
+  # `knownDeps` list which holds the `drvPath`s of the closest derivation marked
+  # as `known` along every edge. This list is inserted into the dependency map
+  # for `drvPath` and every other derivation in its dependecy closure (unless
+  # the information was already present). This means that the known dependency
+  # information for a derivation never has to be recalculated, as long as they
+  # are part of the same stateful computation.
+  #
+  # The upshot is that after calling `insertKnownDeps drvPath`,
+  # `fmap (builtins.getAttr "knownDeps") (getAttr drvPath)` will always succeed.
+  #
+  # Type: str -> stateMonad drvDepMap null
+  insertKnownDeps = drvPathWithContext:
+    let
+      # We no longer need to read from the store, so context is irrelevant, but
+      # we need to check for attr names which requires the absence of context.
+      drvPath = unsafeDiscardStringContext drvPathWithContext;
+    in
+    bind get (initDepMap:
+      # Get the dependency map's state before we've done anything to obtain the
+      # entry we'll be manipulating later as well as its dependencies.
+      let
+        entryPoint = initDepMap.${drvPath};
+
+        # We don't need to recurse if our direct dependencies either have their
+        # knownDeps list already populated or are known dependencies themselves.
+        depsPrecalculated =
+          builtins.partition
+            (dep:
+              initDepMap.${dep}.known
+              || initDepMap.${dep} ? knownDeps
+            )
+            entryPoint.deps;
+
+        # If a direct dependency is known, it goes right to our known dependency
+        # list. If it is unknown, we can copy its knownDeps list into our own.
+        initiallyKnownDeps =
+          builtins.concatLists (
+            builtins.map
+              (dep:
+                if initDepMap.${dep}.known
+                then [ dep ]
+                else initDepMap.${dep}.knownDeps
+              )
+              depsPrecalculated.right
+          );
+      in
+
+      # If the information was already calculated before, we can exit right away
+      if entryPoint ? knownDeps
+      then pure null
+      else
+        after
+          # For all unknown direct dependencies which don't have a `knownDeps`
+          # list, we call ourselves recursively to populate it. Since this is
+          # done sequentially in the state monad, we avoid recalculating the
+          # list for the same derivation multiple times.
+          (for_
+            depsPrecalculated.wrong
+            insertKnownDeps)
+          # After this we can obtain the updated dependency map which will have
+          # a `knownDeps` list for all our direct dependencies and update the
+          # entry for the input `drvPath`.
+          (bind
+            get
+            (populatedDepMap:
+              (setAttr drvPath (entryPoint // {
+                knownDeps =
+                  lib.unique (
+                    initiallyKnownDeps
+                      ++ builtins.concatLists (
+                      builtins.map
+                        (dep: populatedDepMap.${dep}.knownDeps)
+                        depsPrecalculated.wrong
+                    )
+                  );
+              }))))
+    );
+
+  # This function puts it all together and is exposed via `__functor`.
+  #
+  # For a list of `drvPath`s, calculate an attribute set which maps every
+  # `drvPath` to a set of the following form:
+  #
+  #     {
+  #       known = true /* if it is in the list of input derivation paths */;
+  #       deps = [
+  #         /* list of derivation paths it depends on directly */
+  #       ];
+  #       knownDeps = [
+  #         /* list of the closest derivation paths marked as known this
+  #            derivation depends on.
+  #         */
+  #       ];
+  #     }
+  knownDrvDepMap = knownDrvPaths:
+    run
+      (plainDrvDepMap knownDrvPaths)
+      (after
+        (for_
+          knownDrvPaths
+          insertKnownDeps)
+        get);
+
+  #
+  # Other things based on knownDrvDepMap
+  #
+
+  # Create a SVG visualizing `knownDrvDepMap`. Nodes are identified by derivation
+  # name, so multiple entries can be collapsed if they have the same name.
+  #
+  # Type: [drv] -> drv
+  knownDependencyGraph = name: drvs:
+    let
+      justName = drvPath:
+        builtins.substring
+          (builtins.stringLength builtins.storeDir + 1 + 32 + 1)
+          (builtins.stringLength drvPath)
+          (unsafeDiscardStringContext drvPath);
+
+      gv = pkgs.writeText "${name}-dependency-analysis.gv" ''
+        digraph depot {
+        ${
+          (lib.concatStringsSep "\n"
+          (lib.mapAttrsToList (name: value:
+            if !value.known then ""
+            else lib.concatMapStringsSep "\n"
+              (knownDep: "  \"${justName name}\" -> \"${justName knownDep}\"")
+              value.knownDeps
+          )
+          (depot.nix.dependency-analyzer (
+            drvsToPaths drvs
+          ))))
+        }
+        }
+      '';
+    in
+
+    pkgs.runCommand "${name}-dependency-analysis.svg"
+      {
+        nativeBuildInputs = [
+          pkgs.buildPackages.graphviz
+        ];
+      }
+      "dot -Tsvg < ${gv} > $out";
+in
+
+{
+  __functor = _: knownDrvDepMap;
+
+  inherit knownDependencyGraph plainDrvDepMap drvsToPaths;
+}
diff --git a/nix/dependency-analyzer/examples/ci-targets.nix b/nix/dependency-analyzer/examples/ci-targets.nix
new file mode 100644
index 000000000000..597abd410961
--- /dev/null
+++ b/nix/dependency-analyzer/examples/ci-targets.nix
@@ -0,0 +1,12 @@
+{ depot, lib, ... }:
+
+(
+  depot.nix.dependency-analyzer.knownDependencyGraph
+    "depot"
+    depot.ci.targets
+).overrideAttrs (old: {
+  # Causes an infinite recursion via ci.targets otherwise
+  meta = lib.recursiveUpdate (old.meta or { }) {
+    ci.skip = true;
+  };
+})
diff --git a/nix/dependency-analyzer/examples/lisp.nix b/nix/dependency-analyzer/examples/lisp.nix
new file mode 100644
index 000000000000..775eb9ab573f
--- /dev/null
+++ b/nix/dependency-analyzer/examples/lisp.nix
@@ -0,0 +1,5 @@
+{ depot, lib, ... }:
+
+depot.nix.dependency-analyzer.knownDependencyGraph "3p-lisp" (
+  builtins.filter lib.isDerivation (builtins.attrValues depot.third_party.lisp)
+)
diff --git a/nix/dependency-analyzer/tests/default.nix b/nix/dependency-analyzer/tests/default.nix
new file mode 100644
index 000000000000..79ac127e922f
--- /dev/null
+++ b/nix/dependency-analyzer/tests/default.nix
@@ -0,0 +1,36 @@
+{ depot, lib, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    assertEq
+    it
+    ;
+
+  inherit (depot.nix.dependency-analyzer)
+    plainDrvDepMap
+    drvsToPaths
+    ;
+
+  knownDrvs = drvsToPaths (
+    builtins.filter lib.isDerivation (builtins.attrValues depot.third_party.lisp)
+  );
+  exampleMap = plainDrvDepMap knownDrvs;
+
+  # These will be needed to index into the attribute set which can't have context
+  # in the attribute names.
+  knownDrvsNoContext = builtins.map builtins.unsafeDiscardStringContext knownDrvs;
+in
+
+runTestsuite "dependency-analyzer" [
+  (it "checks plainDrvDepMap properties" [
+    (assertEq "all known drvs are marked known"
+      (builtins.all (drv: exampleMap.${drv}.known) knownDrvsNoContext)
+      true)
+    (assertEq "no unknown drv is marked known"
+      (builtins.all (entry: !entry.known) (
+        builtins.attrValues (builtins.removeAttrs exampleMap knownDrvsNoContext)
+      ))
+      true)
+  ])
+]
diff --git a/nix/drvSeqL/default.nix b/nix/drvSeqL/default.nix
new file mode 100644
index 000000000000..6437e1a043a5
--- /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 000000000000..a64022791462
--- /dev/null
+++ b/nix/emptyDerivation/OWNERS
@@ -0,0 +1 @@
+Profpatsch
diff --git a/nix/emptyDerivation/default.nix b/nix/emptyDerivation/default.nix
new file mode 100644
index 000000000000..f808aa228dd5
--- /dev/null
+++ b/nix/emptyDerivation/default.nix
@@ -0,0 +1,22 @@
+{ depot, pkgs, localSystem, ... }:
+
+let
+  emptyDerivation = import ./emptyDerivation.nix {
+    inherit pkgs;
+    inherit (pkgs) stdenv;
+    inherit (depot.nix) getBins;
+    system = localSystem;
+  };
+
+  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 000000000000..d7de7ccfbc4c
--- /dev/null
+++ b/nix/emptyDerivation/emptyDerivation.nix
@@ -0,0 +1,36 @@
+{ stdenv, system, 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";
+    inherit 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 000000000000..a73842882499
--- /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 000000000000..d2c39dd39894
--- /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 000000000000..e354b176c885
--- /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 000000000000..e0f5ab426364
--- /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 000000000000..fbdb30b38eac
--- /dev/null
+++ b/nix/lazy-deps/default.nix
@@ -0,0 +1,91 @@
+# 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, lib, ... }:
+
+let
+  inherit (builtins) attrNames attrValues mapAttrs;
+  inherit (lib) fix 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
+fix (self:
+
+# 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.runCommandNoCC "lazy-dispatch"
+{
+  passthru.overrideDeps = newTools: self (tools // newTools);
+  passthru.tools = tools;
+
+  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}" "''${@}"
+  '';
+
+  # Access this to get a compatible nix-shell
+  passthru.devShell = pkgs.mkShellNoCC {
+    name = "${self.name}-shell";
+    packages = [ self ];
+  };
+}
+  ''
+    # Write the dispatch code
+    target=$out/bin/__dispatch
+    mkdir -p "$(dirname "$target")"
+    echo "$text" > $target
+    chmod +x $target
+
+    # Add symlinks from all the tools to the dispatch
+    ${concatStringsSep "\n" (map link (attrNames tools))}
+
+    # Check that it's working-ish
+    ${pkgs.stdenv.shellDryRun} $target
+  ''
+)
diff --git a/nix/mergePatch/default.nix b/nix/mergePatch/default.nix
new file mode 100644
index 000000000000..d56106925a65
--- /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 000000000000..c854b5695502
--- /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 000000000000..dcd688a8b0b3
--- /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 000000000000..2e9580706346
--- /dev/null
+++ b/nix/nint/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/nix/nint/README.md b/nix/nint/README.md
new file mode 100644
index 000000000000..369a8276199a
--- /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 000000000000..0087fc041603
--- /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 000000000000..abb0153c3ad2
--- /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/nix-1p/README.md b/nix/nix-1p/README.md
new file mode 100644
index 000000000000..e7cf1e2d90d0
--- /dev/null
+++ b/nix/nix-1p/README.md
@@ -0,0 +1,601 @@
+Nix - A One Pager
+=================
+
+[Nix](https://nixos.org/nix/), the package manager, is built on and with Nix,
+the language. This page serves as a fast intro to most of the (small) language.
+
+Unless otherwise specified, the word "Nix" refers only to the language below.
+
+Please file an issue if something in here confuses you or you think something
+important is missing.
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Overview](#overview)
+- [Language constructs](#language-constructs)
+    - [Primitives / literals](#primitives--literals)
+    - [Operators](#operators)
+    - [Variable bindings](#variable-bindings)
+    - [Functions](#functions)
+        - [Multiple arguments (currying)](#multiple-arguments-currying)
+        - [Multiple arguments (attribute sets)](#multiple-arguments-attribute-sets)
+    - [`if ... then ... else ...`](#if--then--else-)
+    - [`inherit` keyword](#inherit-keyword)
+    - [`with` statements](#with-statements)
+    - [`import` / `NIX_PATH` / `<entry>`](#import--nix_path--entry)
+    - [`or` expressions](#or-expressions)
+- [Standard libraries](#standard-libraries)
+    - [`builtins`](#builtins)
+    - [`pkgs.lib`](#pkgslib)
+    - [`pkgs` itself](#pkgs-itself)
+- [Derivations](#derivations)
+- [Nix Idioms](#nix-idioms)
+    - [File lambdas](#file-lambdas)
+    - [`callPackage`](#callpackage)
+    - [Overrides / Overlays](#overrides--overlays)
+
+<!-- markdown-toc end -->
+
+
+# Overview
+
+Nix is:
+
+*   **purely functional**. It has no concept of sequential steps being executed,
+    any dependency between operations is established by depending on *data* from
+    previous operations.
+
+    Everything in Nix is an expression, meaning that every directive returns
+    some kind of data.
+
+    Evaluating a Nix expression *yields a single data structure*, it does not
+    execute a sequence of operations.
+
+    Every Nix file evaluates to a *single expression*.
+*   **lazy**. It will only evaluate expressions when their result is actually
+    requested.
+
+    For example, the builtin function `throw` causes evaluation to stop.
+    Entering the following expression works fine however, because we never
+    actually ask for the part of the structure that causes the `throw`.
+
+    ```nix
+    let attrs = { a = 15; b = builtins.throw "Oh no!"; };
+    in "The value of 'a' is ${toString attrs.a}"
+    ```
+*   **purpose-built**. Nix only exists to be the language for Nix, the package
+    manager. While people have occasionally used it for other use-cases, it is
+    explicitly not a general-purpose language.
+
+# Language constructs
+
+This section describes the language constructs in Nix. It is a small language
+and most of these should be self-explanatory.
+
+## Primitives / literals
+
+Nix has a handful of data types which can be represented literally in source
+code, similar to many other languages.
+
+```nix
+# numbers
+42
+1.72394
+
+# strings & paths
+"hello"
+./some-file.json
+
+# strings support interpolation
+"Hello ${name}"
+
+# multi-line strings (common prefix whitespace is dropped)
+''
+first line
+second line
+''
+
+# lists (note: no commas!)
+[ 1 2 3 ]
+
+# attribute sets (field access with dot syntax)
+{ a = 15; b = "something else"; }
+
+# recursive attribute sets (fields can reference each other)
+rec { a = 15; b = a * 2; }
+```
+
+## Operators
+
+Nix has several operators, most of which are unsurprising:
+
+| Syntax               | Description                                                                 |
+|----------------------|-----------------------------------------------------------------------------|
+| `+`, `-`, `*`, `/`   | Numerical operations                                                        |
+| `+`                  | String concatenation                                                        |
+| `++`                 | List concatenation                                                          |
+| `==`                 | Equality                                                                    |
+| `>`, `>=`, `<`, `<=` | Ordering comparators                                                        |
+| `&&`                 | Logical `AND`                                                               |
+| <code>&vert;&vert;</code> | Logical `OR`                                                           |
+| `e1 -> e2`           | Logical implication (i.e. <code>!e1 &vert;&vert; e2</code>)                 |
+| `!`                  | Boolean negation                                                            |
+| `set.attr`           | Access attribute `attr` in attribute set `set`                              |
+| `set ? attribute`    | Test whether attribute set contains an attribute                            |
+| `left // right`      | Merge `left` & `right` attribute sets, with the right set taking precedence |
+
+Make sure to understand the `//`-operator, as it is used quite a lot and is
+probably the least familiar one.
+
+## Variable bindings
+
+Bindings in Nix are introduced locally via `let` expressions, which make some
+variables available within a given scope.
+
+For example:
+
+```nix
+let
+  a = 15;
+  b = 2;
+in a * b
+
+# yields 30
+```
+
+Variables are immutable. This means that after defining what `a` or `b` are, you
+can not *modify* their value in the scope in which they are available.
+
+You can nest `let`-expressions to shadow variables.
+
+Variables are *not* available outside of the scope of the `let` expression.
+There are no global variables.
+
+## Functions
+
+All functions in Nix are anonymous lambdas. This means that they are treated
+just like data. Giving them names is accomplished by assigning them to
+variables, or setting them as values in an attribute set (more on that below).
+
+```
+# simple function
+# declaration is simply the argument followed by a colon
+name: "Hello ${name}"
+```
+
+### Multiple arguments (currying)
+
+Technically any Nix function can only accept **one argument**. Sometimes
+however, a function needs multiple arguments. This is achieved in Nix via
+[currying][], which means to create a function with one argument, that returns a
+function with another argument, that returns ... and so on.
+
+For example:
+
+```nix
+name: age: "${name} is ${toString age} years old"
+```
+
+An additional benefit of this approach is that you can pass one parameter to a
+curried function, and receive back a function that you can re-use (similar to
+partial application):
+
+```nix
+let
+  multiply = a: b: a * b;
+  doubleIt = multiply 2; # at this point we have passed in the value for 'a' and
+                         # receive back another function that still expects 'b'
+in
+  doubleIt 15
+
+# yields 30
+```
+
+### Multiple arguments (attribute sets)
+
+Another way of specifying multiple arguments to a function in Nix is to make it
+accept an attribute set, which enables multiple other features:
+
+```nix
+{ name, age }: "${name} is ${toString age} years old"
+```
+
+Using this method, we gain the ability to specify default arguments (so that
+callers can omit them):
+
+```nix
+{ name, age ? 42 }: "${name} is ${toString age} years old"
+
+```
+
+Or in practice:
+
+```nix
+let greeter =  { name, age ? 42 }: "${name} is ${toString age} years old";
+in greeter { name = "Slartibartfast"; }
+
+# yields "Slartibartfast is 42 years old"
+# (note: Slartibartfast is actually /significantly/ older)
+```
+
+Additionally we can introduce an ellipsis using `...`, meaning that we can
+accept an attribute set as our input that contains more variables than are
+needed for the function.
+
+```nix
+let greeter = { name, age, ... }: "${name} is ${toString age} years old";
+    person = {
+      name = "Slartibartfast";
+      age = 42;
+      # the 'email' attribute is not expected by the 'greeter' function ...
+      email = "slartibartfast@magrath.ea";
+    };
+in greeter person # ... but the call works due to the ellipsis.
+```
+
+Nix also supports binding the whole set of passed in attributes to a
+parameter using the `@` syntax:
+
+```nix
+let func = { name, age, ... }@args: builtins.attrNames args;
+in func {
+    name = "Slartibartfast";
+    age = 42;
+    email = "slartibartfast@magrath.ea";
+}
+
+# yields: [ "age" "email" "name" ]
+```
+
+**Warning:** Combining the `@` syntax with default arguments can lead
+to surprising behaviour, as the passed attributes are bound verbatim.
+This means that defaulted arguments are not included in the bound
+attribute set:
+
+```nix
+({ a ? 1, b }@args: args.a) { b = 1; }
+# throws: error: attribute 'a' missing
+
+({ a ? 1, b }@args: args.a) { b = 1; a = 2; }
+# => 2
+```
+
+## `if ... then ... else ...`
+
+Nix has simple conditional support. Note that `if` is an **expression** in Nix,
+which means that both branches must be specified.
+
+```nix
+if someCondition
+then "it was true"
+else "it was false"
+```
+
+## `inherit` keyword
+
+The `inherit` keyword is used in attribute sets or `let` bindings to "inherit"
+variables from the parent scope.
+
+In short, a statement like `inherit foo;` expands to `foo = foo;`.
+
+Consider this example:
+
+```nix
+let
+  name = "Slartibartfast";
+  # ... other variables
+in {
+  name = name; # set the attribute set key 'name' to the value of the 'name' var
+  # ... other attributes
+}
+```
+
+The `name = name;` line can be replaced with `inherit name;`:
+
+```nix
+let
+  name = "Slartibartfast";
+  # ... other variables
+in {
+  inherit name;
+  # ... other attributes
+}
+```
+
+This is often convenient, especially because inherit supports multiple variables
+at the same time as well as "inheritance" from other attribute sets:
+
+```nix
+{
+  inherit name age; # equivalent to `name = name; age = age;`
+  inherit (otherAttrs) email; # equivalent to `email = otherAttrs.email`;
+}
+```
+
+## `with` statements
+
+The `with` statement "imports" all attributes from an attribute set into
+variables of the same name:
+
+```nix
+let attrs = { a = 15; b = 2; };
+in with attrs; a + b # 'a' and 'b' become variables in the scope following 'with'
+```
+
+## `import` / `NIX_PATH` / `<entry>`
+
+Nix files can import each other by using the `import` keyword and a literal
+path:
+
+```nix
+# assuming there is a file lib.nix with some useful functions
+let myLib = import ./lib.nix;
+in myLib.usefulFunction 42
+```
+
+Nix files often begin with a function header to pass parameters into the rest of
+the file, so you will often see imports of the form `import ./some-file { ... }`.
+
+Nix has a concept of a `NIX_PATH` (similar to the standard `PATH` environment
+variable) which contains named aliases for file paths containing Nix
+expressions.
+
+In a standard Nix installation, several [channels][] will be present (for
+example `nixpkgs` or `nixos-unstable`) on the `NIX_PATH`.
+
+`NIX_PATH` entries can be accessed using the `<entry>` syntax, which simply
+evaluates to their file path:
+
+```nix
+<nixpkgs>
+# might yield something like `/home/tazjin/.nix-defexpr/channels/nixpkgs`
+```
+
+This is commonly used to import from channels:
+
+```nix
+let pkgs = import <nixpkgs> {};
+in pkgs.something
+```
+
+## `or` expressions
+
+Nix has a keyword called `or` which can be used to access a value from an
+attribute set while providing a fallback to a default value.
+
+The syntax is simple:
+
+```nix
+# Access an existing attribute
+let set = { a = 42; };
+in set.a or 23
+```
+
+Since the attribute `a` exists, this will return `42`.
+
+
+```nix
+# ... or fall back to a default if there is no such key
+let set = { };
+in set.a or 23
+```
+
+Since the attribute `a` does not exist, this will fall back to returning the
+default value `23`.
+
+Note that `or` expressions also work for nested attribute set access.
+
+# Standard libraries
+
+Yes, libraries, plural.
+
+Nix has three major things that could be considered its standard library and
+while there's a lot of debate to be had about this point, you still need to know
+all three.
+
+## `builtins`
+
+Nix comes with several functions that are baked into the language. These work
+regardless of which other Nix code you may or may not have imported.
+
+Most of these functions are implemented in the Nix interpreter itself, which
+means that they are rather fast when compared to some of the equivalents which
+are implemented in Nix itself.
+
+The Nix manual has [a section listing all `builtins`][builtins] and their usage.
+
+Examples of builtins that you will commonly encounter include, but are not
+limited to:
+
+* `derivation` (see [Derivations](#derivations))
+* `toJSON` / `fromJSON`
+* `toString`
+* `toPath` / `fromPath`
+
+The builtins also include several functions that have the (spooky) ability to
+break Nix' evaluation purity. No functions written in Nix itself can do this.
+
+Examples of those include:
+
+* `fetchGit` which can fetch a git-repository using the environment's default
+  git/ssh configuration
+* `fetchTarball` which can fetch & extract archives without having to specify
+  hashes
+
+Read through the manual linked above to get the full overview.
+
+## `pkgs.lib`
+
+The Nix package set, commonly referred to by Nixers simply as [nixpkgs][],
+contains a child attribute set called `lib` which provides a large number of
+useful functions.
+
+The canonical definition of these functions is [their source code][lib-src]. I
+wrote a tool ([nixdoc][]) in 2018 which generates manual entries for these
+functions, however not all of the files are included as of July 2019.
+
+See the [Nixpkgs manual entry on `lib`][lib-manual] for the documentation.
+
+These functions include various utilities for dealing with the data types in Nix
+(lists, attribute sets, strings etc.) and it is useful to at least skim through
+them to familiarise yourself with what is available.
+
+```nix
+{ pkgs ? import <nixpkgs> {} }:
+
+with pkgs.lib; # bring contents pkgs.lib into scope
+
+strings.toUpper "hello"
+
+# yields "HELLO"
+```
+
+## `pkgs` itself
+
+The Nix package set itself does not just contain packages, but also many useful
+functions which you might run into while creating new Nix packages.
+
+One particular subset of these that stands out are the [trivial builders][],
+which provide utilities for writing text files or shell scripts, running shell
+commands and capturing their output and so on.
+
+```nix
+{ pkgs ? import <nixpkgs> {} }:
+
+pkgs.writeText "hello.txt" "Hello dear reader!"
+
+# yields a derivation which creates a text file with the above content
+```
+
+# Derivations
+
+When a Nix expression is evaluated it may yield one or more *derivations*.
+Derivations describe a single build action that, when run, places one or more
+outputs (whether they be files or folders) in the Nix store.
+
+The builtin function `derivation` is responsible for creating derivations at a
+lower level. Usually when Nix users create derivations they will use the
+higher-level functions such as [stdenv.mkDerivation][smkd].
+
+Please see the manual [on derivations][drv-manual] for more information, as the
+general build logic is out of scope for this document.
+
+# Nix Idioms
+
+There are several idioms in Nix which are not technically part of the language
+specification, but will commonly be encountered in the wild.
+
+This section is an (incomplete) list of them.
+
+## File lambdas
+
+It is customary to start every file with a function header that receives the
+files dependencies, instead of importing them directly in the file.
+
+Sticking to this pattern lets users of your code easily change out, for example,
+the specific version of `nixpkgs` that is used.
+
+A common file header pattern would look like this:
+
+```nix
+{ pkgs ? import <nixpkgs> {} }:
+
+# ... 'pkgs' is then used in the code
+```
+
+In some sense, you might consider the function header of a file to be its "API".
+
+## `callPackage`
+
+Building on the previous pattern, there is a custom in nixpkgs of specifying the
+dependencies of your file explicitly instead of accepting the entire package
+set.
+
+For example, a file containing build instructions for a tool that needs the
+standard build environment and `libsvg` might start like this:
+
+```nix
+# my-funky-program.nix
+{ stdenv, libsvg }:
+
+stdenv.mkDerivation { ... }
+```
+
+Any time a file follows this header pattern it is probably meant to be imported
+using a special function called `callPackage` which is part of the top-level
+package set (as well as certain subsets, such as `haskellPackages`).
+
+```nix
+{ pkgs ? import <nixpkgs> {} }:
+
+let my-funky-program = pkgs.callPackage ./my-funky-program.nix {};
+in # ... something happens with my-funky-program
+```
+
+The `callPackage` function looks at the expected arguments (via
+`builtins.functionArgs`) and passes the appropriate keys from the set in which
+it is defined as the values for each corresponding argument.
+
+## Overrides / Overlays
+
+One of the most powerful features of Nix is that the representation of all build
+instructions as data means that they can easily be *overridden* to get a
+different result.
+
+For example, assuming there is a package `someProgram` which is built without
+our favourite configuration flag (`--mimic-threaten-tag`) we might override it
+like this:
+
+```nix
+someProgram.overrideAttrs(old: {
+    configureFlags = old.configureFlags or [] ++ ["--mimic-threaten-tag"];
+})
+```
+
+This pattern has a variety of applications of varying complexity. The top-level
+package set itself can have an `overlays` argument passed to it which may add
+new packages to the imported set.
+
+Note the use of the `or` operator to default to an empty list if the
+original flags do not include `configureFlags`. This is required in
+case a package does not set any flags by itself.
+
+Since this can change in a package over time, it is useful to guard
+against it using `or`.
+
+For a slightly more advanced example, assume that we want to import `<nixpkgs>`
+but have the modification above be reflected in the imported package set:
+
+```nix
+let
+  overlay = (final: prev: {
+    someProgram = prev.someProgram.overrideAttrs(old: {
+      configureFlags = old.configureFlags or [] ++ ["--mimic-threaten-tag"];
+    });
+  });
+in import <nixpkgs> { overlays = [ overlay ]; }
+```
+
+The overlay function receives two arguments, `final` and `prev`. `final` is
+the [fixed point][fp] of the overlay's evaluation, i.e. the package set
+*including* the new packages and `prev` is the "original" package set.
+
+See the Nix manual sections [on overrides][] and [on overlays][] for more
+details (note: the convention has moved away from using `self` in favor of
+`final`, and `prev` instead of `super`, but the documentation has not been
+updated to reflect this).
+
+[currying]: https://en.wikipedia.org/wiki/Currying
+[builtins]: https://nixos.org/nix/manual/#ssec-builtins
+[nixpkgs]: https://github.com/NixOS/nixpkgs
+[lib-src]: https://github.com/NixOS/nixpkgs/tree/master/lib
+[nixdoc]: https://github.com/tazjin/nixdoc
+[lib-manual]: https://nixos.org/nixpkgs/manual/#sec-functions-library
+[channels]: https://nixos.org/nix/manual/#sec-channels
+[trivial builders]: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/trivial-builders.nix
+[smkd]: https://nixos.org/nixpkgs/manual/#chap-stdenv
+[drv-manual]: https://nixos.org/nix/manual/#ssec-derivation
+[fp]: https://github.com/NixOS/nixpkgs/blob/master/lib/fixed-points.nix
+[on overrides]: https://nixos.org/nixpkgs/manual/#sec-overrides
+[on overlays]: https://nixos.org/nixpkgs/manual/#chap-overlays
diff --git a/nix/nix-1p/default.nix b/nix/nix-1p/default.nix
new file mode 100644
index 000000000000..6cc71b9548fc
--- /dev/null
+++ b/nix/nix-1p/default.nix
@@ -0,0 +1,16 @@
+# The canonical source location of nix-1p is //nix/nix-1p in the TVL
+# depot: https://code.tvl.fyi/about/nix/nix-1p
+#
+# This file configures TVL CI to mirror the subtree to GitHub.
+{ depot ? { }, pkgs ? import <nixpkgs> { }, ... }:
+
+(pkgs.runCommandLocal "nix-1p" { } ''
+  mkdir $out
+  cp ${./README.md} $out/README.md
+'').overrideAttrs (_: {
+  meta.ci.extraSteps.github = depot.tools.releases.filteredGitPush {
+    filter = ":/nix/nix-1p";
+    remote = "git@github.com:tazjin/nix-1p.git";
+    ref = "refs/heads/master";
+  };
+})
diff --git a/nix/readTree/README.md b/nix/readTree/README.md
new file mode 100644
index 000000000000..5d430d1cfced
--- /dev/null
+++ b/nix/readTree/README.md
@@ -0,0 +1,95 @@
+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:
+
+* 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 other than a set, else the children are *not traversed*.
+* A folder can opt out from readTree completely by containing a
+  `.skip-tree` file. The content of the file is not read. These
+  folders will be missing completely from the readTree structure.
+* 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.
+* 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 000000000000..15680082cd61
--- /dev/null
+++ b/nix/readTree/default.nix
@@ -0,0 +1,326 @@
+# 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;
+      # skip hidden files, except for those that contain special instructions to readTree
+      isVisible = f: f == ".skip-subtree" || f == ".skip-tree" || (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 != { } && builtins ? scopedImport # For tvix
+        then builtins.scopedImport scopedArgs path
+        else import path;
+      pathType = builtins.typeOf importedFile;
+    in
+    if pathType != "lambda"
+    then 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;
+
+  # Internal implementation of readTree, which handles things like the
+  # skipping of trees and subtrees.
+  #
+  # This method returns an attribute sets with either of two shapes:
+  #
+  # { ok = ...; }    # a tree was read successfully
+  # { skip = true; } # a tree was skipped
+  #
+  # The higher-level `readTree` method assembles the final attribute
+  # set out of these results at the top-level, and the internal
+  # `children` implementation unwraps and processes nested trees.
+  readTreeImpl = { args, initPath, rootDir, parts, argsFilter, scopedArgs }:
+    let
+      dir = readDirVisible initPath;
+
+      # Determine whether any part of this tree should be skipped.
+      #
+      # Adding a `.skip-subtree` file will still allow the import of
+      # the current node's "default.nix" file, but stop recursion
+      # there.
+      #
+      # Adding a `.skip-tree` file will completely ignore the folder
+      # in which this file is located.
+      skipTree = hasAttr ".skip-tree" dir;
+      skipSubtree = skipTree || hasAttr ".skip-subtree" dir;
+
+      joinChild = c: initPath + ("/" + c);
+
+      self =
+        if rootDir
+        then { __readTree = [ ]; }
+        else importFile args scopedArgs initPath parts argsFilter;
+
+      # Import subdirectories of the current one, unless any skip
+      # instructions exist.
+      #
+      # 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";
+      filteredChildren = map
+        (c: {
+          name = c;
+          value = readTreeImpl {
+            inherit argsFilter scopedArgs;
+            args = args;
+            initPath = (joinChild c);
+            rootDir = false;
+            parts = (parts ++ [ c ]);
+          };
+        })
+        (filter filterDir (attrNames dir));
+
+      # Remove skipped children from the final set, and unwrap the
+      # result set.
+      children =
+        if skipSubtree then [ ]
+        else map ({ name, value }: { inherit name; value = value.ok; }) (filter (child: child.value ? ok) filteredChildren);
+
+      # Import Nix files
+      nixFiles =
+        if skipSubtree 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 skipTree
+    then { skip = true; }
+    else {
+      ok =
+        if isAttrs nodeValue
+        then merge nodeValue (allChildren // (marker parts allChildren))
+        else nodeValue;
+    };
+
+  # Top-level implementation of readTree itself.
+  readTree = args:
+    let
+      tree = readTreeImpl args;
+    in
+    if tree ? skip
+    then throw "Top-level folder has a .skip-tree marker and could not be read by readTree!"
+    else tree.ok;
+
+  # 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 000000000000..46952a4ca635
--- /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 000000000000..6f9eb02effb9
--- /dev/null
+++ b/nix/readTree/tests/default.nix
@@ -0,0 +1,139 @@
+{ 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-tree/a is read"
+      tree-tl.skip-tree.a
+      "a is read normally")
+    (assertEq "skip-tree does not contain b"
+      (builtins.attrNames tree-tl.skip-tree)
+      [ "__readTree" "__readTreeChildren" "a" ])
+    (assertEq "skip-tree children list does not contain b"
+      tree-tl.skip-tree.__readTreeChildren
+      [ "a" ])
+
+    (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 000000000000..27d87242189d
--- /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 000000000000..f964fa583c43
--- /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 000000000000..54b3c0503f12
--- /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 000000000000..a1919442d86d
--- /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 000000000000..03c7492bb6b1
--- /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 000000000000..5bd3e36b5397
--- /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 000000000000..5bd3e36b5397
--- /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 000000000000..5bd3e36b5397
--- /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 000000000000..5bd3e36b5397
--- /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 000000000000..95d13d3c2750
--- /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 000000000000..2ee2d648f042
--- /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 000000000000..6003b5398305
--- /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 000000000000..c469533fbe5b
--- /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 000000000000..2008c2d2411f
--- /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 000000000000..8c57f2c16137
--- /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 000000000000..cf0ac2c8f3cf
--- /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 000000000000..a586ce534ce2
--- /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 000000000000..06216c417be9
--- /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 000000000000..3d9f241cddb5
--- /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 000000000000..87271ba5e101
--- /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 000000000000..a586ce534ce2
--- /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 000000000000..06216c417be9
--- /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 000000000000..3d9f241cddb5
--- /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-tree-traversal/skip-tree/a/default.nix b/nix/readTree/tests/test-tree-traversal/skip-tree/a/default.nix
new file mode 100644
index 000000000000..186488be3c9b
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/skip-tree/a/default.nix
@@ -0,0 +1 @@
+_: "a is read normally"
diff --git a/nix/readTree/tests/test-tree-traversal/skip-tree/b/.skip-tree b/nix/readTree/tests/test-tree-traversal/skip-tree/b/.skip-tree
new file mode 100644
index 000000000000..34936b45d141
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/skip-tree/b/.skip-tree
@@ -0,0 +1 @@
+b subfolder should be skipped completely
diff --git a/nix/readTree/tests/test-tree-traversal/skip-tree/b/default.nix b/nix/readTree/tests/test-tree-traversal/skip-tree/b/default.nix
new file mode 100644
index 000000000000..7903f8e95a38
--- /dev/null
+++ b/nix/readTree/tests/test-tree-traversal/skip-tree/b/default.nix
@@ -0,0 +1 @@
+throw "b is skipped completely"
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 000000000000..4681253af831
--- /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 000000000000..f46ee2a35565
--- /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 000000000000..c5e830c83409
--- /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.runCommand "${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 000000000000..76fffdce7b0d
--- /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 000000000000..23b9a6330370
--- /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 000000000000..f82b544224ee
--- /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 000000000000..8b02ed86d8ea
--- /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 000000000000..2e9580706346
--- /dev/null
+++ b/nix/sparseTree/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/nix/sparseTree/default.nix b/nix/sparseTree/default.nix
new file mode 100644
index 000000000000..7bfa47237869
--- /dev/null
+++ b/nix/sparseTree/default.nix
@@ -0,0 +1,73 @@
+# 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.runCommand "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/stateMonad/default.nix b/nix/stateMonad/default.nix
new file mode 100644
index 000000000000..209412e09952
--- /dev/null
+++ b/nix/stateMonad/default.nix
@@ -0,0 +1,76 @@
+# Simple state monad represented as
+#
+#     stateMonad s a = s -> { state : s; value : a }
+#
+{ ... }:
+
+rec {
+  #
+  # Monad
+  #
+
+  # Type: stateMonad s a -> (a -> stateMonad s b) -> stateMonad s b
+  bind = action: f: state:
+    let
+      afterAction = action state;
+    in
+    (f afterAction.value) afterAction.state;
+
+  # Type: stateMonad s a -> stateMonad s b -> stateMonad s b
+  after = action1: action2: state: action2 (action1 state).state;
+
+  # Type: stateMonad s (stateMonad s a) -> stateMonad s a
+  join = action: bind action (action': action');
+
+  # Type: [a] -> (a -> stateMonad s b) -> stateMonad s null
+  for_ = xs: f:
+    builtins.foldl'
+      (laterAction: x:
+        after (f x) laterAction
+      )
+      (pure null)
+      xs;
+
+  #
+  # Applicative
+  #
+
+  # Type: a -> stateMonad s a
+  pure = value: state: { inherit state value; };
+
+  # TODO(sterni): <*>, lift2, …
+
+  #
+  # Functor
+  #
+
+  # Type: (a -> b) -> stateMonad s a -> stateMonad s b
+  fmap = f: action: bind action (result: pure (f result));
+
+  #
+  # State Monad
+  #
+
+  # Type: (s -> s) -> stateMonad s null
+  modify = f: state: { value = null; state = f state; };
+
+  # Type: stateMonad s s
+  get = state: { value = state; inherit state; };
+
+  # Type: s -> stateMonad s null
+  set = new: modify (_: new);
+
+  # Type: str -> stateMonad set set.${str}
+  getAttr = attr: fmap (state: state.${attr}) get;
+
+  # Type: str -> (any -> any) -> stateMonad s null
+  modifyAttr = attr: f: modify (state: state // {
+    ${attr} = f state.${attr};
+  });
+
+  # Type: str -> any -> stateMonad s null
+  setAttr = attr: value: modifyAttr attr (_: value);
+
+  # Type: s -> stateMonad s a -> a
+  run = state: action: (action state).value;
+}
diff --git a/nix/stateMonad/tests/default.nix b/nix/stateMonad/tests/default.nix
new file mode 100644
index 000000000000..c3cb5c99b550
--- /dev/null
+++ b/nix/stateMonad/tests/default.nix
@@ -0,0 +1,110 @@
+{ depot, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    ;
+
+  inherit (depot.nix.stateMonad)
+    pure
+    run
+    join
+    fmap
+    bind
+    get
+    set
+    modify
+    after
+    for_
+    getAttr
+    setAttr
+    modifyAttr
+    ;
+
+  runStateIndependent = run (throw "This should never be evaluated!");
+in
+
+runTestsuite "stateMonad" [
+  (it "behaves correctly independent of state" [
+    (assertEq "pure" (runStateIndependent (pure 21)) 21)
+    (assertEq "join pure" (runStateIndependent (join (pure (pure 42)))) 42)
+    (assertEq "fmap pure" (runStateIndependent (fmap (builtins.mul 2) (pure 21))) 42)
+    (assertEq "bind pure" (runStateIndependent (bind (pure 12) (x: pure x))) 12)
+  ])
+  (it "behaves correctly with an integer state" [
+    (assertEq "get" (run 42 get) 42)
+    (assertEq "after set get" (run 21 (after (set 42) get)) 42)
+    (assertEq "after modify get" (run 21 (after (modify (builtins.mul 2)) get)) 42)
+    (assertEq "fmap get" (run 40 (fmap (builtins.add 2) get)) 42)
+    (assertEq "stateful sum list"
+      (run 0 (after
+        (for_
+          [
+            15
+            12
+            10
+            5
+          ]
+          (x: modify (builtins.add x)))
+        get))
+      42)
+  ])
+  (it "behaves correctly with an attr set state" [
+    (assertEq "getAttr" (run { foo = 42; } (getAttr "foo")) 42)
+    (assertEq "after setAttr getAttr"
+      (run { foo = 21; } (after (setAttr "foo" 42) (getAttr "foo")))
+      42)
+    (assertEq "after modifyAttr getAttr"
+      (run { foo = 10.5; }
+        (after
+          (modifyAttr "foo" (builtins.mul 4))
+          (getAttr "foo")))
+      42)
+    (assertEq "fmap getAttr"
+      (run { foo = 21; } (fmap (builtins.mul 2) (getAttr "foo")))
+      42)
+    (assertEq "after setAttr to insert getAttr"
+      (run { } (after (setAttr "foo" 42) (getAttr "foo")))
+      42)
+    (assertEq "insert permutations"
+      (run
+        {
+          a = 2;
+          b = 3;
+          c = 5;
+        }
+        (after
+          (bind get
+            (state:
+              let
+                names = builtins.attrNames state;
+              in
+              for_ names (name1:
+                for_ names (name2:
+                  # this is of course a bit silly, but making it more cumbersome
+                  # makes sure the test exercises more of the code.
+                  (bind (getAttr name1)
+                    (value1:
+                      (bind (getAttr name2)
+                        (value2:
+                          setAttr "${name1}_${name2}" (value1 * value2)))))))))
+          get))
+      {
+        a = 2;
+        b = 3;
+        c = 5;
+        a_a = 4;
+        a_b = 6;
+        a_c = 10;
+        b_a = 6;
+        b_b = 9;
+        b_c = 15;
+        c_c = 25;
+        c_a = 10;
+        c_b = 15;
+      }
+    )
+  ])
+]
diff --git a/nix/tag/default.nix b/nix/tag/default.nix
new file mode 100644
index 000000000000..2955656323fa
--- /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 != { })
+      "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 000000000000..e0085b4837a1
--- /dev/null
+++ b/nix/tag/tests.nix
@@ -0,0 +1,99 @@
+{ depot, lib, verifyTag, discr, discrDef, match, matchLam }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    assertEq
+    assertThrows
+    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"; })
+    (assertThrows "throws failing to match"
+      (discr [
+        { fish = x: x == 42; }
+      ] 21))
+  ];
+
+  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 000000000000..363f717db6de
--- /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 000000000000..2e9580706346
--- /dev/null
+++ b/nix/utils/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/nix/utils/default.nix b/nix/utils/default.nix
new file mode 100644
index 000000000000..0c6c88fafd5a
--- /dev/null
+++ b/nix/utils/default.nix
@@ -0,0 +1,160 @@
+{ 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;
+
+  /* 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
+    pathType
+    isDirectory
+    isRegularFile
+    isSymlink
+    ;
+}
diff --git a/nix/utils/tests/.skip-subtree b/nix/utils/tests/.skip-subtree
new file mode 100644
index 000000000000..1efb1b9476ba
--- /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 000000000000..344a1771d7d9
--- /dev/null
+++ b/nix/utils/tests/default.nix
@@ -0,0 +1,99 @@
+{ depot, lib, ... }:
+
+let
+  inherit (depot.nix.runTestsuite)
+    runTestsuite
+    it
+    assertEq
+    assertThrows
+    assertDoesNotThrow
+    ;
+
+  inherit (depot.nix.utils)
+    isDirectory
+    isRegularFile
+    isSymlink
+    pathType
+    storePathName
+    ;
+
+  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)
+  ];
+in
+
+runTestsuite "nix.utils" [
+  pathPredicates
+  storePathNameTests
+]
diff --git a/nix/utils/tests/directory/file b/nix/utils/tests/directory/file
new file mode 100644
index 000000000000..e69de29bb2d1
--- /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 000000000000..cfa0a46515b5
--- /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 000000000000..6d0450cc2421
--- /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 000000000000..cd5d01792aa4
--- /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 000000000000..5d6ba5247eff
--- /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 000000000000..f8d9ce3659f4
--- /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 000000000000..ee3dc7a3ed54
--- /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 000000000000..5169b01386ea
--- /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 000000000000..1f53b4e4ff51
--- /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 000000000000..ed26cf197e1e
--- /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 000000000000..55355913a9f4
--- /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 000000000000..232a2dc60808
--- /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 000000000000..98e6642e2dd8
--- /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 000000000000..cb9fc08287fb
--- /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 000000000000..71673e7ab63c
--- /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 000000000000..30ed50f8327b
--- /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 000000000000..6b03ed65ceb7
--- /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 000000000000..05a302cc6b9d
--- /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 000000000000..fcbcf6415fad
--- /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 000000000000..0c7ec2418802
--- /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 000000000000..1199c56cfb94
--- /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 000000000000..f850b536455c
--- /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 == cfg.Branch {
+		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/buildkite/.gitignore b/ops/buildkite/.gitignore
new file mode 100644
index 000000000000..41c1b3346260
--- /dev/null
+++ b/ops/buildkite/.gitignore
@@ -0,0 +1,2 @@
+.envrc
+.terraform*
diff --git a/ops/buildkite/README.md b/ops/buildkite/README.md
new file mode 100644
index 000000000000..9d31a53fd36a
--- /dev/null
+++ b/ops/buildkite/README.md
@@ -0,0 +1,24 @@
+Buildkite configuration
+=======================
+
+This contains Terraform configuration for setting up our Buildkite
+pipelines.
+
+Each pipeline (such as the one for depot itself, or exported subsets
+of the depot) needs some static configuration stored in Buildkite.
+
+Through `//tools/depot-deps` a `tf-buildkite` binary is made available
+which contains a Terraform binary pre-configured with the correct
+providers. This is automatically on your `$PATH` through `direnv`.
+
+However, secrets still need to be loaded to access the Terraform state
+and speak to the Buildkite API. These are available to certain users
+through `//ops/secrets`.
+
+This can be done with separate direnv configuration, for example:
+
+```
+# //ops/buildkite/.envrc
+source_up
+eval $(age --decrypt -i ~/.ssh/id_ed25519 $(git rev-parse --show-toplevel)/ops/secrets/tf-buildkite.age)
+```
diff --git a/ops/buildkite/default.nix b/ops/buildkite/default.nix
new file mode 100644
index 000000000000..0d39bc06010d
--- /dev/null
+++ b/ops/buildkite/default.nix
@@ -0,0 +1,14 @@
+{ depot, lib, pkgs, ... }:
+
+depot.nix.readTree.drvTargets rec {
+  terraform = pkgs.terraform.withPlugins (p: [
+    p.buildkite
+  ]);
+
+  validate = depot.tools.checks.validateTerraform {
+    inherit terraform;
+    name = "buildkite";
+    src = lib.cleanSource ./.;
+    env.BUILDKITE_API_TOKEN = "ci-dummy";
+  };
+}
diff --git a/ops/buildkite/steps-depot.yml b/ops/buildkite/steps-depot.yml
new file mode 100644
index 000000000000..011b29977144
--- /dev/null
+++ b/ops/buildkite/steps-depot.yml
@@ -0,0 +1,6 @@
+---
+steps:
+  - label: ":buildkite:"
+    key: ":init:"
+    command: |
+      buildkite-agent pipeline upload ops/pipelines/static-pipeline.yaml
diff --git a/ops/buildkite/steps-tvix.yml b/ops/buildkite/steps-tvix.yml
new file mode 100644
index 000000000000..a6e9f13b166a
--- /dev/null
+++ b/ops/buildkite/steps-tvix.yml
@@ -0,0 +1,4 @@
+---
+steps:
+  - label: ":buildkite: Upload pipeline"
+    command: "buildkite-agent pipeline upload"
diff --git a/ops/buildkite/steps-tvl-kit.yml b/ops/buildkite/steps-tvl-kit.yml
new file mode 100644
index 000000000000..a6e9f13b166a
--- /dev/null
+++ b/ops/buildkite/steps-tvl-kit.yml
@@ -0,0 +1,4 @@
+---
+steps:
+  - label: ":buildkite: Upload pipeline"
+    command: "buildkite-agent pipeline upload"
diff --git a/ops/buildkite/tvl.tf b/ops/buildkite/tvl.tf
new file mode 100644
index 000000000000..4c45909a0c36
--- /dev/null
+++ b/ops/buildkite/tvl.tf
@@ -0,0 +1,48 @@
+# Buildkite configuration for TVL.
+
+terraform {
+  required_providers {
+    buildkite = {
+      source = "buildkite/buildkite"
+    }
+  }
+
+  backend "s3" {
+    endpoint = "https://objects.dc-sto1.glesys.net"
+    bucket   = "tvl-state"
+    key      = "terraform/tvl-buildkite"
+    region   = "glesys"
+
+    skip_credentials_validation = true
+    skip_region_validation      = true
+    skip_metadata_api_check     = true
+  }
+}
+
+provider "buildkite" {
+  organization = "tvl"
+}
+
+resource "buildkite_pipeline" "depot" {
+  name           = "depot"
+  description    = "Run full CI pipeline of the depot, TVL's monorepo."
+  repository     = "https://cl.tvl.fyi/depot"
+  steps          = file("./steps-depot.yml")
+  default_branch = "refs/heads/canon"
+}
+
+resource "buildkite_pipeline" "tvix" {
+  name           = "tvix"
+  description    = "Tvix, an exported subset of TVL depot"
+  repository     = "https://code.tvl.fyi/depot.git:workspace=views/tvix.git"
+  steps          = file("./steps-tvix.yml")
+  default_branch = "canon"
+}
+
+resource "buildkite_pipeline" "tvl_kit" {
+  name           = "tvl-kit"
+  description    = "TVL Kit, an exported subset of TVL depot"
+  repository     = "https://code.tvl.fyi/depot.git:workspace=views/kit.git"
+  steps          = file("./steps-tvl-kit.yml")
+  default_branch = "canon"
+}
diff --git a/ops/deploy-whitby/default.nix b/ops/deploy-whitby/default.nix
new file mode 100644
index 000000000000..aafe798cbfa7
--- /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 000000000000..756aa7ae0801
--- /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 000000000000..2290299fe46c
--- /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 000000000000..33fe6d6fe71e
--- /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.runCommand "${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 000000000000..44cabab29bd8
--- /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 000000000000..d1961c6a7a82
--- /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 000000000000..da46752f13ba
--- /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 000000000000..6d785c0817fc
--- /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 000000000000..bfe4eedeb6ac
--- /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 000000000000..1b88600f197f
--- /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 000000000000..f3bec7a3a242
--- /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 000000000000..684636de3084
--- /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 000000000000..de8e8f12ee98
--- /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 000000000000..00f61a93604b
--- /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 000000000000..e511e1f6b686
--- /dev/null
+++ b/ops/glesys/default.nix
@@ -0,0 +1,15 @@
+{ depot, lib, pkgs, ... }:
+
+depot.nix.readTree.drvTargets rec {
+  # Provide a Terraform wrapper with the right provider installed.
+  terraform = pkgs.terraform.withPlugins (_: [
+    depot.third_party.terraform-provider-glesys
+  ]);
+
+  validate = depot.tools.checks.validateTerraform {
+    inherit terraform;
+    name = "glesys";
+    src = lib.cleanSource ./.;
+    env.GLESYS_TOKEN = "ci-dummy";
+  };
+}
diff --git a/ops/glesys/dns-nixery-dev.tf b/ops/glesys/dns-nixery-dev.tf
new file mode 100644
index 000000000000..53a421d20e34
--- /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-tvix-dev.tf b/ops/glesys/dns-tvix-dev.tf
new file mode 100644
index 000000000000..f4570326031c
--- /dev/null
+++ b/ops/glesys/dns-tvix-dev.tf
@@ -0,0 +1,47 @@
+# DNS configuration for tvix.dev
+
+resource "glesys_dnsdomain" "tvix_dev" {
+  name = "tvix.dev"
+}
+
+resource "glesys_dnsdomain_record" "tvix_dev_apex_A" {
+  domain = glesys_dnsdomain.tvix_dev.id
+  host   = "@"
+  type   = "A"
+  data   = var.whitby_ipv4
+}
+
+resource "glesys_dnsdomain_record" "tvix_dev_apex_AAAA" {
+  domain = glesys_dnsdomain.tvix_dev.id
+  host   = "@"
+  type   = "AAAA"
+  data   = var.whitby_ipv6
+}
+
+resource "glesys_dnsdomain_record" "tvix_dev_docs_CNAME" {
+  domain = glesys_dnsdomain.tvix_dev.id
+  host   = "docs"
+  type   = "CNAME"
+  data   = "whitby.tvl.fyi."
+}
+
+resource "glesys_dnsdomain_record" "tvix_dev_NS1" {
+  domain = glesys_dnsdomain.tvix_dev.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns1.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvix_dev_NS2" {
+  domain = glesys_dnsdomain.tvix_dev.id
+  host   = "@"
+  type   = "NS"
+  data   = "ns2.namesystem.se."
+}
+
+resource "glesys_dnsdomain_record" "tvix_dev_NS3" {
+  domain = glesys_dnsdomain.tvix_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 000000000000..26105e9fdc38
--- /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
+}
+
+# Explicit records for all services running on whitby
+resource "glesys_dnsdomain_record" "tvl_fyi_whitby_services" {
+  domain   = glesys_dnsdomain.tvl_fyi.id
+  type     = "CNAME"
+  data     = "whitby.tvl.fyi."
+  host     = each.key
+  for_each = toset(local.whitby_services)
+}
+
+# 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 000000000000..0f397193d7ae
--- /dev/null
+++ b/ops/glesys/dns-tvl-su.tf
@@ -0,0 +1,140 @@
+# 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
+}
+
+# Explicit records for all services running on whitby
+resource "glesys_dnsdomain_record" "tvl_su_whitby_services" {
+  domain   = glesys_dnsdomain.tvl_su.id
+  type     = "CNAME"
+  data     = "whitby.tvl.su."
+  host     = each.key
+  for_each = toset(local.whitby_services)
+}
+
+# Explicit records for corp-only services running on whitby.
+resource "glesys_dnsdomain_record" "tvl_su_corp_whitby_services" {
+  domain = glesys_dnsdomain.tvl_su.id
+  type   = "CNAME"
+  data   = "whitby.tvl.su."
+  host   = each.key
+  for_each = toset([
+    "tvixbolt",
+  ])
+}
+
+resource "glesys_dnsdomain_record" "tvl_su_inbox_CNAME" {
+  domain = glesys_dnsdomain.tvl_su.id
+  type   = "CNAME"
+  data   = "sanduny.tvl.su."
+  host   = "inbox.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 000000000000..cd5ea9f4fd34
--- /dev/null
+++ b/ops/glesys/main.tf
@@ -0,0 +1,87 @@
+# 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"
+}
+
+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"
+}
+
+locals {
+  # Hostnames of all public services on whitby
+  whitby_services = [
+    "at",
+    "atward",
+    "auth",
+    "b",
+    "cache",
+    "cl",
+    "code",
+    "cs",
+    "deploys",
+    "images",
+    "static",
+    "status",
+    "todo",
+  ]
+}
diff --git a/ops/journaldriver/.gitignore b/ops/journaldriver/.gitignore
new file mode 100644
index 000000000000..29e65519ba35
--- /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 000000000000..39b193e0d8bc
--- /dev/null
+++ b/ops/journaldriver/Cargo.lock
@@ -0,0 +1,640 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "build-env"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e068f31938f954b695423ecaf756179597627d0828c0d3e48c0a722a8b23cf9e"
+
+[[package]]
+name = "cc"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "crimp"
+version = "4087.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ead2c83f7d1f9b8e5a6f7a25985d0d1759ccd2cd72abb1eee2db65d05e12b39"
+dependencies = [
+ "curl",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "cstr-argument"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bd9c8e659a473bce955ae5c35b116af38af11a7acb0b480e01f3ed348aeb40"
+dependencies = [
+ "cfg-if",
+ "memchr",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.59+curl-7.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared 0.3.1",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8469d0d40519bc608ec6863f1cc88f3f1deee15913f2f3b3e573d81ed38cccc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
+dependencies = [
+ "hermit-abi",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
+
+[[package]]
+name = "journaldriver"
+version = "5656.0.0"
+dependencies = [
+ "anyhow",
+ "crimp",
+ "env_logger",
+ "lazy_static",
+ "log",
+ "medallion",
+ "pkg-config",
+ "serde",
+ "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.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "libsystemd-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d28ad38d7bee81aabd41201ee7d36df8d7f76aa0a455c77d5c365c4669b4b4b6"
+dependencies = [
+ "build-env",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[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 = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "openssl"
+version = "0.10.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "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.80"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rustix"
+version = "0.36.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+
+[[package]]
+name = "schannel"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "systemd"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da95085b9c6eedbcf0b828302a3483a84bdbf772158e586b787092112008fd1f"
+dependencies = [
+ "cstr-argument",
+ "foreign-types 0.5.0",
+ "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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
+dependencies = [
+ "itoa",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "time-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[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"
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
diff --git a/ops/journaldriver/Cargo.toml b/ops/journaldriver/Cargo.toml
new file mode 100644
index 000000000000..65510d870566
--- /dev/null
+++ b/ops/journaldriver/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "journaldriver"
+version = "5656.0.0"
+authors = ["Vincent Ambo <tazjin@tvl.su>"]
+license = "GPL-3.0-or-later"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0"
+crimp = "4087.0"
+env_logger = "0.10"
+lazy_static = "1.4"
+log = "0.4"
+medallion = "2.5"
+serde = { version = "1.0", features = [ "derive" ] }
+serde_json = "1.0"
+systemd = "0.5"
+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 000000000000..4dc9de0f617b
--- /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 000000000000..79eb1001bf09
--- /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 000000000000..2a3836c35826
--- /dev/null
+++ b/ops/journaldriver/default.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+
+  buildInputs = with pkgs; [
+    pkg-config
+    openssl
+    systemd.dev
+  ];
+}
diff --git a/ops/journaldriver/src/main.rs b/ops/journaldriver/src/main.rs
new file mode 100644
index 000000000000..4c404e607e5b
--- /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 000000000000..6f5045d6a5cd
--- /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 000000000000..017878c614d0
--- /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 000000000000..fd72daa87d25
--- /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/keycloak/.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 000000000000..9506bd4aa046
--- /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 000000000000..94ed912dc936
--- /dev/null
+++ b/ops/keycloak/default.nix
@@ -0,0 +1,14 @@
+{ depot, lib, pkgs, ... }:
+
+depot.nix.readTree.drvTargets rec {
+  # Provide a Terraform wrapper with the right provider installed.
+  terraform = pkgs.terraform.withPlugins (p: [
+    p.keycloak
+  ]);
+
+  validate = depot.tools.checks.validateTerraform {
+    inherit terraform;
+    name = "keycloak";
+    src = lib.cleanSource ./.;
+  };
+}
diff --git a/ops/keycloak/main.tf b/ops/keycloak/main.tf
new file mode 100644
index 000000000000..923ac1939712
--- /dev/null
+++ b/ops/keycloak/main.tf
@@ -0,0 +1,44 @@
+# Configure TVL Keycloak instance.
+#
+# TODO(tazjin): Configure 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"
+
+  smtp_server {
+    from              = "tvlbot@tazj.in"
+    from_display_name = "The Virus Lounge"
+    host              = "127.0.0.1"
+    port              = "25"
+    reply_to          = "depot@tvl.su"
+    ssl               = false
+    starttls          = false
+  }
+}
diff --git a/ops/keycloak/user_sources.tf b/ops/keycloak/user_sources.tf
new file mode 100644
index 000000000000..01307fff8df5
--- /dev/null
+++ b/ops/keycloak/user_sources.tf
@@ -0,0 +1,44 @@
+# 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).
+
+variable "github_client_secret" {
+  type = string
+}
+
+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",
+  ]
+}
+
+# keycloak_oidc_identity_provider.github will be destroyed
+# (because keycloak_oidc_identity_provider.github is not in configuration)
+resource "keycloak_oidc_identity_provider" "github" {
+  alias                 = "github"
+  provider_id           = "github"
+  client_id             = "6d7f8bb2e82bb6739556"
+  client_secret         = var.github_client_secret
+  realm                 = keycloak_realm.tvl.id
+  backchannel_supported = false
+  gui_order             = "1"
+  store_token           = false
+  sync_mode             = "IMPORT"
+  trust_email           = true
+
+  # These default to built-in values for the `github` provider_id.
+  authorization_url = ""
+  token_url         = ""
+}
diff --git a/ops/kontemplate/.gitignore b/ops/kontemplate/.gitignore
new file mode 100644
index 000000000000..53a04aab3a89
--- /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 000000000000..94a9ed024d38
--- /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 000000000000..803a1c4f16fb
--- /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 000000000000..e4258c53dd09
--- /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 000000000000..2d0378a0ec23
--- /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 000000000000..471eb246cf2f
--- /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 000000000000..a619c8cfddcc
--- /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 000000000000..d589c99b4eaf
--- /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 000000000000..0ffa3cd81f24
--- /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 000000000000..2c81f83c0919
--- /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 000000000000..6cf86183229e
--- /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 000000000000..dd7804f719c3
--- /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 000000000000..29d3334fb54d
--- /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 000000000000..12244e1ab174
--- /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 000000000000..df30d3d8cbe3
--- /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 000000000000..2a51352571a6
--- /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 000000000000..040a19aaba25
--- /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 000000000000..5920b2720780
--- /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 000000000000..42676c3028fe
--- /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 000000000000..8459fd30405b
--- /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 000000000000..5215c559c136
--- /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 000000000000..af27bdc455bf
--- /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 000000000000..1190869c3fda
--- /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 000000000000..7693968bd550
--- /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 000000000000..4e870161796d
--- /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 000000000000..1444dd491249
--- /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 000000000000..32da205108de
--- /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 000000000000..5401ac91e5a3
--- /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 000000000000..87370569c46e
--- /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 000000000000..70e2365f1710
--- /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 000000000000..9f300a492080
--- /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 000000000000..f0188f9dbde3
--- /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 000000000000..733d5e16789a
--- /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 000000000000..a40fa83b0867
--- /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 000000000000..fe04765401be
--- /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 000000000000..bfd87c0201e7
--- /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 000000000000..e55d42465c6b
--- /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 000000000000..6a3dbd5efe87
--- /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 000000000000..6cd974dd9324
--- /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 000000000000..f7fbcb433de5
--- /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 000000000000..a8f0c670a603
--- /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 000000000000..9d9fc8d1ffa7
--- /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 000000000000..4f7997bd69d2
--- /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 000000000000..8155e174fe00
--- /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 000000000000..06f1cfc630c3
--- /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 000000000000..56fa1e3fc9c5
--- /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 000000000000..53c56081758c
--- /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 000000000000..e2371d76e742
--- /dev/null
+++ b/ops/machines/all-systems.nix
@@ -0,0 +1,26 @@
+{ depot, ... }:
+
+(with depot.ops.machines; [
+  sanduny
+  whitby
+]) ++
+
+(with depot.users.tazjin.nixos; [
+  camden
+  frog
+  tverskoy
+  zamalek
+]) ++
+
+(with depot.users.grfn.system.system; [
+  yeren
+  mugwump
+  ogopogo
+]) ++
+
+(with depot.users.wpcarro.nixos; [
+  ava
+  kyoko
+  marcus
+  tarasco
+])
diff --git a/ops/machines/sanduny/default.nix b/ops/machines/sanduny/default.nix
new file mode 100644
index 000000000000..4db6311ceb6b
--- /dev/null
+++ b/ops/machines/sanduny/default.nix
@@ -0,0 +1,130 @@
+# 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 "cgit.nix")
+    (mod "depot-inbox.nix")
+    (mod "depot-replica.nix")
+    (mod "journaldriver.nix")
+    (mod "known-hosts.nix")
+    (mod "tvl-cache.nix")
+    (mod "tvl-users.nix")
+    (mod "www/inbox.tvl.su.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";
+  };
+
+  # Allow Gerrit to replicate depot to /var/lib/depot
+  services.depot.replica.enable = true;
+
+  # Run git serving tools locally ...
+  services.depot.cgit = {
+    enable = true;
+    repo = "/var/lib/depot";
+  };
+
+  # Serve public-inbox ...
+  services.depot.inbox.enable = true;
+
+  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";
+    };
+  };
+
+  tvl.cache.enable = true;
+
+  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 000000000000..4581a80d617d
--- /dev/null
+++ b/ops/machines/whitby/OWNERS
@@ -0,0 +1,5 @@
+set noparent
+
+# Want in on this list? Try paying!
+lukegb
+tazjin
diff --git a/ops/machines/whitby/README.md b/ops/machines/whitby/README.md
new file mode 100644
index 000000000000..55287c541256
--- /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 000000000000..f2a459047163
--- /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.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/tvix.dev.nix")
+    (mod "www/tvixbolt.tvl.su.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 = {
+    tmp.useTmpfs = 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;
+    settings = {
+      max-jobs = lib.mkDefault 64;
+      secret-key-files = "/run/agenix/nix-cache-priv";
+
+      trusted-users = [
+        "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;
+    settings = {
+      PasswordAuthentication = false;
+      KbdInteractiveAuthentication = 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";
+      };
+
+      depot-replica-key = {
+        file = secretFile "depot-replica-key";
+        mode = "0500";
+        owner = "git";
+        group = "git";
+        path = "/var/lib/git/.ssh/id_ed25519";
+      };
+    };
+
+  # 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;
+      user = "git"; # run as the same user as gerrit
+    };
+
+    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;
+
+    settings = {
+      server = {
+        http_port = 4723; # "graf" on phone keyboard
+        domain = "status.tvl.su";
+        root_url = "https://status.tvl.su";
+      };
+
+      analytics.reporting_enabled = false;
+
+      "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;
+      };
+
+      "auth.anonymous" = {
+        enabled = true;
+        org_name = "The Virus Lounge";
+        org_role = "Viewer";
+      };
+
+      "auth.basic".enabled = false;
+
+      auth = {
+        oauth_auto_login = true;
+        disable_login_form = true;
+      };
+    };
+
+    provision = {
+      enable = true;
+      datasources.settings.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;
+
+    settings = {
+      http-port = 5925; # kycl
+      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";
+    };
+  };
+
+  zramSwap.enable = true;
+
+  system.stateVersion = "20.03";
+}
diff --git a/ops/modules/.skip-subtree b/ops/modules/.skip-subtree
new file mode 100644
index 000000000000..09520f8c831f
--- /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 000000000000..595b4c3344c6
--- /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 000000000000..f345a08e3131
--- /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 000000000000..c504906b2b94
--- /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 000000000000..ad53a63f7f16
--- /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.nix b/ops/modules/cgit.nix
new file mode 100644
index 000000000000..fc3f17158579
--- /dev/null
+++ b/ops/modules/cgit.nix
@@ -0,0 +1,55 @@
+# Configuration for running the TVL cgit instance using thttpd.
+{ config, depot, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.cgit;
+
+  userConfig =
+    if builtins.isNull cfg.user then {
+      DynamicUser = true;
+    } else {
+      User = cfg.user;
+      Group = cfg.user;
+    };
+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;
+    };
+
+    repo = mkOption {
+      description = "Path to depot's .git folder on the machine";
+      type = types.str;
+      default = "/var/lib/gerrit/git/depot.git/";
+    };
+
+    user = mkOption {
+      description = ''
+        User to use for the cgit service. It is expected that this is
+        also the name of the user's primary group.
+      '';
+
+      type = with types; nullOr str;
+      default = null;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.cgit = {
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart = "on-failure";
+
+        ExecStart = depot.web.cgit-tvl.override {
+          inherit (cfg) port repo;
+        };
+      } // userConfig;
+    };
+  };
+}
diff --git a/ops/modules/clbot.nix b/ops/modules/clbot.nix
new file mode 100644
index 000000000000..bdddff6c810b
--- /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) runCommand;
+
+  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 (runCommand "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 000000000000..11514a437a42
--- /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 000000000000..d747e8e1319a
--- /dev/null
+++ b/ops/modules/default.nix
@@ -0,0 +1,2 @@
+# Make readTree happy at this level.
+_: { }
diff --git a/ops/modules/depot-inbox.nix b/ops/modules/depot-inbox.nix
new file mode 100644
index 000000000000..6110e47250cc
--- /dev/null
+++ b/ops/modules/depot-inbox.nix
@@ -0,0 +1,148 @@
+# public-inbox configuration for depot@tvl.su
+#
+# The account itself is a Yandex 360 account in the tvl.su organisation, which
+# is accessed via IMAP. Yandex takes care of spam filtering for us, so there is
+# no particular SpamAssassin or other configuration.
+{ config, depot, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.inbox;
+
+  imapConfig = pkgs.writeText "offlineimaprc" ''
+    [general]
+    accounts = depot
+
+    [Account depot]
+    localrepository = Local
+    remoterepository = Remote
+
+    [Repository Local]
+    type = Maildir
+    localfolders = /var/lib/public-inbox/depot-imap
+
+    [Repository Remote]
+    type = IMAP
+    ssl = yes
+    sslcacertfile = /etc/ssl/certs/ca-bundle.crt
+    remotehost = imap.yandex.ru
+    remoteuser = depot@tvl.su
+    remotepassfile = /var/run/agenix/depot-inbox-imap
+  '';
+in
+{
+  options.services.depot.inbox = with lib; {
+    enable = mkEnableOption "Enable public-inbox for depot@tvl.su";
+
+    depotPath = mkOption {
+      description = "path to local depot replica";
+      type = types.str;
+      default = "/var/lib/depot";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # Having nginx *and* other services use ACME certificates for the
+    # same hostname is unsupported in NixOS without resorting to doing
+    # all ACME configuration manually.
+    #
+    # To work around this, we duplicate the TLS certificate used by
+    # nginx to a location that is readable by public-inbox daemons.
+    systemd.services.inbox-cert-sync = {
+      startAt = "daily";
+
+      script = ''
+        ${pkgs.coreutils}/bin/install -D -g ${config.users.groups."public-inbox".name} -m 0440 \
+          /var/lib/acme/inbox.tvl.su/fullchain.pem /var/lib/public-inbox/tls/fullchain.pem
+
+        ${pkgs.coreutils}/bin/install -D -g ${config.users.groups."public-inbox".name} -m 0440 \
+          /var/lib/acme/inbox.tvl.su/key.pem /var/lib/public-inbox/tls/key.pem
+      '';
+    };
+
+    services.public-inbox = {
+      enable = true;
+
+      http.enable = true;
+      http.port = 8053;
+
+      imap = {
+        enable = true;
+        port = 993;
+        cert = "/var/lib/public-inbox/tls/fullchain.pem";
+        key = "/var/lib/public-inbox/tls/key.pem";
+      };
+
+      nntp = {
+        enable = true;
+        port = 563;
+        cert = "/var/lib/public-inbox/tls/fullchain.pem";
+        key = "/var/lib/public-inbox/tls/key.pem";
+      };
+
+      inboxes.depot = rec {
+        address = [
+          "depot@tvl.su" # primary address
+          "depot@tazj.in" # legacy address
+        ];
+
+        description = "TVL depot development (mail to depot@tvl.su)";
+        coderepo = [ "depot" ];
+        url = "https://inbox.tvl.su/depot";
+
+        watch = [
+          "maildir:/var/lib/public-inbox/depot-imap/INBOX/"
+        ];
+
+        newsgroup = "su.tvl.depot";
+      };
+
+      settings.coderepo.depot = {
+        dir = cfg.depotPath;
+        cgitUrl = "https://code.tvl.fyi";
+      };
+
+      settings.publicinbox = {
+        wwwlisting = "all";
+        nntpserver = [ "inbox.tvl.su" ];
+        imapserver = "inbox.tvl.su";
+
+        depot.obfuscate = true;
+        noObfuscate = [
+          "tvl.su"
+          "tvl.fyi"
+        ];
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = [
+      993 # imap
+      563 # nntp
+    ];
+
+    age.secrets.depot-inbox-imap = {
+      file = depot.ops.secrets."depot-inbox-imap.age";
+      mode = "0440";
+      group = config.users.groups."public-inbox".name;
+    };
+
+    systemd.services.offlineimap-depot = {
+      description = "download mail for depot@tvl.su";
+      wantedBy = [ "multi-user.target" ];
+      startAt = "minutely";
+
+      script = ''
+        mkdir -p /var/lib/public-inbox/depot-imap
+        ${pkgs.offlineimap}/bin/offlineimap -c ${imapConfig}
+      '';
+
+      serviceConfig = {
+        Type = "oneshot";
+
+        # Run in the same user context as public-inbox itself to avoid
+        # permissions trouble.
+        User = config.users.users."public-inbox".name;
+        Group = config.users.groups."public-inbox".name;
+      };
+    };
+  };
+}
diff --git a/ops/modules/depot-replica.nix b/ops/modules/depot-replica.nix
new file mode 100644
index 000000000000..b71f10409a2b
--- /dev/null
+++ b/ops/modules/depot-replica.nix
@@ -0,0 +1,45 @@
+# Configuration for receiving a depot replica from Gerrit's
+# replication plugin.
+#
+# This only prepares the user and folder for receiving the replica,
+# but Gerrit configuration still needs to be modified in addition.
+{ config, depot, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.replica;
+in
+{
+  options.services.depot.replica = with lib; {
+    enable = mkEnableOption "Receive depot git replica from Gerrit";
+
+    key = mkOption {
+      description = "Public key to use for replication";
+      type = types.str;
+      default = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFFab9O1xaQ1TCyn+CxmXHexdlLzURREG+UR3Qdi3BvH";
+    };
+
+    path = mkOption {
+      description = "Replication destination path (will be created)";
+      type = types.str;
+      default = "/var/lib/depot";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    users.groups.depot = { };
+
+    users.users.depot = {
+      group = "depot";
+      isSystemUser = true;
+      createHome = true;
+      home = cfg.path;
+      homeMode = "755"; # everyone can read depot
+      openssh.authorizedKeys.keys = lib.singleton cfg.key;
+      shell = pkgs.bashInteractive; # gerrit needs to run shell commands
+    };
+
+    environment.systemPackages = [
+      pkgs.git
+    ];
+  };
+}
diff --git a/ops/modules/gerrit-queue.nix b/ops/modules/gerrit-queue.nix
new file mode 100644
index 000000000000..4468bcf1c5f9
--- /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 000000000000..0819c52a8de7
--- /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 000000000000..4591ebf0f078
--- /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/ --require-auth";
+      };
+    };
+  };
+}
diff --git a/ops/modules/journaldriver.nix b/ops/modules/journaldriver.nix
new file mode 100644
index 000000000000..0d6b0bcc7f63
--- /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 000000000000..9ea689178e3b
--- /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 000000000000..059dee57940b
--- /dev/null
+++ b/ops/modules/monorepo-gerrit.nix
@@ -0,0 +1,174 @@
+# 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.runCommand "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"
+      "replication"
+    ];
+
+    plugins = with depot.third_party.gerrit_plugins; [
+      code-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.
+      };
+
+      plugin.code-owners = {
+        # A Code-Review +2 vote is required from a code owner.
+        requiredApproval = "Code-Review+2";
+        # The OWNERS check can be overriden using an Owners-Override vote.
+        overrideApproval = "Owners-Override+1";
+        # People implicitly approve their own changes automatically.
+        enableImplicitApprovals = "TRUE";
+      };
+
+      # 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;
+      };
+    };
+
+    # Replication of the depot repository to secondary machines, for
+    # serving cgit/josh.
+    replicationSettings = {
+      gerrit.replicateOnStartup = true;
+
+      remote.sanduny = {
+        url = "depot@sanduny.tvl.su:/var/lib/depot";
+        projects = "depot";
+      };
+    };
+  };
+
+  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 000000000000..4122f9ebbf3b
--- /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 000000000000..53dec452ec83
--- /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 000000000000..3a86e9e41690
--- /dev/null
+++ b/ops/modules/open_eid.nix
@@ -0,0 +1,54 @@
+# NixOS module to configure the Estonian e-ID software.
+{ pkgs, ... }:
+
+{
+  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
+  '';
+
+  # Configure Firefox (in case users set `programs.firefox.enable = true;`)
+  programs.firefox = {
+    # Allow a possibly installed "Web eID" extension to do native messaging with
+    # the "web-eid-app" native component.
+    # Users not using `programs.firefox.enable` can override their firefox
+    # derivation, by setting `extraNativeMessagingHosts = [ pkgs.web-eid-app ]`.
+    nativeMessagingHosts.euwebid = true;
+    # Configure Firefox to load smartcards via p11kit-proxy.
+    # Users not using `programs.firefox.enable` can override their firefox
+    # derivation, by setting
+    # `extraPolicies.SecurityDevices.p11-kit-proxy "${pkgs.p11-kit}/lib/p11-kit-proxy.so"`.
+    policies.SecurityDevices.p11-kit-proxy = "${pkgs.p11-kit}/lib/p11-kit-proxy.so";
+  };
+
+  # Chromium users need a symlink to their (slightly different) .json file
+  # in the native messaging hosts' manifest file location.
+  environment.etc."chromium/native-messaging-hosts/eu.webeid.json".source = "${pkgs.web-eid-app}/share/web-eid/eu.webeid.json";
+  environment.etc."opt/chrome/native-messaging-hosts/eu.webeid.json".source = "${pkgs.web-eid-app}/share/web-eid/eu.webeid.json";
+
+  environment.systemPackages = with pkgs; [
+    libdigidocpp.bin # provides digidoc-tool(1)
+    qdigidoc
+
+    # Wrapper script to tell to Chrome/Chromium to use p11-kit-proxy to load
+    # security devices, so they can be used for TLS client auth.
+    # 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.
+    (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
+    '')
+  ];
+}
diff --git a/ops/modules/owothia.nix b/ops/modules/owothia.nix
new file mode 100644
index 000000000000..b9746c172090
--- /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 000000000000..e23dd028ab3c
--- /dev/null
+++ b/ops/modules/panettone.nix
@@ -0,0 +1,119 @@
+{ 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;
+      };
+    };
+
+    systemd.services.panettone-fixer = {
+      description = "Restart panettone regularly to work around b/225";
+      wantedBy = [ "multi-user.target" ];
+      script = "${pkgs.systemd}/bin/systemctl restart panettone";
+      serviceConfig.Type = "oneshot";
+
+      # We don't exactly know how frequently this occurs, but
+      # _probably_ not more than hourly.
+      startAt = "hourly";
+    };
+  };
+}
diff --git a/ops/modules/paroxysm.nix b/ops/modules/paroxysm.nix
new file mode 100644
index 000000000000..070e7623db09
--- /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 000000000000..349364f9b7ed
--- /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 000000000000..275e2809d793
--- /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 000000000000..869539603578
--- /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 000000000000..f6ce2621757d
--- /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 000000000000..cbf836ab64d5
--- /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 000000000000..87879565542d
--- /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.runCommand "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 000000000000..683818d103b7
--- /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.settings = {
+      trusted-public-keys = [
+        "cache.tvl.su:kjc6KOMupXc1vHVufJUoDUYeLzbwSr9abcAKdn/U1Jk="
+      ];
+
+      substituters = [
+        "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 000000000000..c08cd30f166c
--- /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/db";
+        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 000000000000..004bc5cbfff0
--- /dev/null
+++ b/ops/modules/tvl-users.nix
@@ -0,0 +1,95 @@
+# 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;
+      description = "Alyssa Ross";
+      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;
+    };
+  };
+
+  programs.fish.enable = true;
+
+  environment.systemPackages = with pkgs; [
+    alacritty.terminfo
+    foot.terminfo
+    rxvt-unicode-unwrapped.terminfo
+    kitty.terminfo
+  ];
+}
diff --git a/ops/modules/v4l2loopback.nix b/ops/modules/v4l2loopback.nix
new file mode 100644
index 000000000000..636b2ff6cf27
--- /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 000000000000..6b3672dd75cb
--- /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 000000000000..a068f023658e
--- /dev/null
+++ b/ops/modules/www/auth.tvl.fyi.nix
@@ -0,0 +1,28 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."auth.tvl.fyi" = {
+      serverName = "auth.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        # increase buffer size for large headers
+        proxy_buffers 8 16k;
+        proxy_buffer_size 16k;
+
+        location / {
+          proxy_pass http://localhost:${toString config.services.keycloak.settings.http-port};
+          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 000000000000..45f6c6ed5141
--- /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 000000000000..50fceff0fa40
--- /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 000000000000..99bc008cd6a5
--- /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 000000000000..36422a6c4e38
--- /dev/null
+++ b/ops/modules/www/cl.tvl.fyi.nix
@@ -0,0 +1,34 @@
+{ 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;
+        }
+
+        location = /robots.txt {
+          return 200 'User-agent: *\nAllow: /';
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/code.tvl.fyi.nix b/ops/modules/www/code.tvl.fyi.nix
new file mode 100644
index 000000000000..3f34a9422cb1
--- /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 000000000000..fac814baf064
--- /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 000000000000..ffbe225b58a3
--- /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 000000000000..7d027b2991ab
--- /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/inbox.tvl.su.nix b/ops/modules/www/inbox.tvl.su.nix
new file mode 100644
index 000000000000..38db5d2a8eda
--- /dev/null
+++ b/ops/modules/www/inbox.tvl.su.nix
@@ -0,0 +1,31 @@
+{ config, depot, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."inbox.tvl.su" = {
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        # nginx is incapable of serving a single file at /, hence this hack:
+        location = / {
+          index /landing-page;
+        }
+
+        location = /landing-page {
+          types { } default_type "text/html; charset=utf-8";
+          alias ${depot.web.inbox};
+        }
+
+        # rest of requests is proxied to public-inbox-httpd
+        location / {
+          proxy_pass http://localhost:${toString config.services.public-inbox.http.port};
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/nixery.dev.nix b/ops/modules/www/nixery.dev.nix
new file mode 100644
index 000000000000..05dc88c66a07
--- /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 000000000000..5bf1627be99a
--- /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 000000000000..7312f78ecf42
--- /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 000000000000..7079c602604e
--- /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.settings.server.http_port}";
+      };
+    };
+  };
+}
diff --git a/ops/modules/www/tazj.in.nix b/ops/modules/www/tazj.in.nix
new file mode 100644
index 000000000000..3b80222e0d66
--- /dev/null
+++ b/ops/modules/www/tazj.in.nix
@@ -0,0 +1,49 @@
+# 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;
+      serverAliases = [ "www.tazj.in" ];
+
+      extraConfig = ''
+        location = /en/rss.xml {
+          return 301 https://tazj.in/feed.atom;
+        }
+
+        ${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;
+        }
+
+        location = /predlozhnik {
+          return 302 https://predlozhnik.ru;
+        }
+
+        # 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 000000000000..b53f5437e7ab
--- /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/tvix.dev.nix b/ops/modules/www/tvix.dev.nix
new file mode 100644
index 000000000000..f86f5b3b1e98
--- /dev/null
+++ b/ops/modules/www/tvix.dev.nix
@@ -0,0 +1,39 @@
+{ depot, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."tvix.dev" = {
+      serverName = "tvix.dev";
+      enableACME = true;
+      forceSSL = true;
+      root = depot.tvix.website;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+      '';
+    };
+
+    services.nginx.virtualHosts."docs.tvix.dev" = {
+      serverName = "docs.tvix.dev";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+        location = / {
+          # until we have a better default page here
+          return 301 https://docs.tvix.dev/rust/tvix_eval/index.html;
+        }
+
+        location /rust/ {
+          alias ${depot.tvix.rust-docs}/;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/tvixbolt.tvl.su.nix b/ops/modules/www/tvixbolt.tvl.su.nix
new file mode 100644
index 000000000000..7adddd123632
--- /dev/null
+++ b/ops/modules/www/tvixbolt.tvl.su.nix
@@ -0,0 +1,19 @@
+{ depot, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."tvixbolt.tvl.su" = {
+      root = depot.corp.tvixbolt;
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+      '';
+    };
+  };
+}
diff --git a/ops/modules/www/tvl.fyi.nix b/ops/modules/www/tvl.fyi.nix
new file mode 100644
index 000000000000..59ee1bc27f1a
--- /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 000000000000..a7c4f6a21721
--- /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 000000000000..3d85e4eb9843
--- /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 000000000000..5bd19a47c3b3
--- /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 000000000000..c4013ac13ebc
--- /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 000000000000..18fed3621d4d
--- /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 000000000000..816a37075977
--- /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 000000000000..e289cbab81e0
--- /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 000000000000..1045de896b02
--- /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 000000000000..6b0e32009a66
--- /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 000000000000..927993b486ad
--- /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 000000000000..309f12297744
--- /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 000000000000..a3f94fd23143
--- /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 000000000000..5f02e7edfbb8
--- /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: protoCheck";
+  };
+
+  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.runCommand "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 000000000000..46b41480ed58
--- /dev/null
+++ b/ops/pipelines/static-pipeline.yaml
@@ -0,0 +1,129 @@
+# 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}"
+
+  # Run pipeline for tvix 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: "tvix"
+    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"
+    concurrency_group: 'depot-nix-eval'
+    concurrency: 5 # much more than this and whitby will OOM
+    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 --context cl-annotation
+      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}" \
+        --include "store=/nix/store" \
+        --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 release 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 'release-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 000000000000..e5b6fdb28e32
--- /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 000000000000..c4013ac13ebc
--- /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 000000000000..dc344613d052
--- /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 000000000000..8390b80b86f0
--- /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 000000000000..2389546b1383
--- /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 000000000000..800d2221e492
--- /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 000000000000..6b0e32009a66
--- /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 000000000000..bacd2aeb39e0
--- /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 000000000000..ed35fb03be82
--- /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 000000000000..1f4ea9a58da6
--- /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 000000000000..80f63816f5ba
--- /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 000000000000..e59b86541335
--- /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 000000000000..50c2d1442def
--- /dev/null
+++ b/ops/secrets/besadii.age
Binary files differdiff --git a/ops/secrets/buildkite-agent-token.age b/ops/secrets/buildkite-agent-token.age
new file mode 100644
index 000000000000..66802310bbca
--- /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 000000000000..6ebf3efca7dc
--- /dev/null
+++ b/ops/secrets/buildkite-graphql-token.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw X7cI9stdU1F8M8Mhk/5a4UwU2Ze6rBXuwRDxUTKCTHw
+CnksXNl+VEs2CYiucBeIgfpzpA05VshlECkbmTUZSpI
+-> ssh-ed25519 zcCuhA 7KOsie4KRM0pPKZk8MeDISuX4tT9MAw/5mehSQcNOE8
+UfbpAlKJVhZOH5j4YIw5CVDen7UebTO/S55sLT9tVyc
+-> ssh-ed25519 CpJBgQ EiDs9pCdSnPb4T4HvgF+gdyJ9f5orhtn1OVUp45e3jM
+SlMWEzpi/mMlhfBPzVBn6jZknvjWCbRQMLoJEklJV2w
+-> ssh-ed25519 aXKGcg kiuat73hEcxKvRZ9Gk115LjB3WVgd0h5KrjMOyTRLzw
+CwEmQX6vmi6DnJp/TeYFOSdsfrprHylXAzhnAaQ3aKw
+-> ssh-ed25519 OkGqLg R+moPPGckVPXrAnwQXFPqsizUwK+8UlL2VAA1965d1Y
+J0sxPR2PDqK3k39dSLOzFQkUUZ5cfYqww6NHQ7E4ql4
+-> lb6ND/-grease !D$d P~ Tj.
+HjRsXF0B07o957mq0zRgyHlckismT8UI8KcyFN55ff9FlWpci3+LEcPCb08wtraP
+DSRvOi4
+--- AomJrDQJ4VQghgD6b7ItcPNyiu+cDmNQM31FOqYBbEk
+
0:๹X0b^(:Vr%GTh>~q*	ת;}$
\ 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 000000000000..c9aa988277bf
--- /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 000000000000..c24f8f45d3da
--- /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 000000000000..2cec1f7f36c7
--- /dev/null
+++ b/ops/secrets/clbot.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw ZkAwxhi/ckHaVTnF7bmzOXhQG3HHqw1CpMe6nQL0rHc
+9qnf0AY/inCEvk1VBd4RC3M0kATM/JuIyWxqisjersY
+-> ssh-ed25519 zcCuhA o3PRUMcah5zjj39LtDWpgmBPFtHyx1N9WQz++lFrFEI
+7K1kZHKfmlV5G/xVbgeOuLAO2iXKqcEyRYm+YfTvURs
+-> ssh-ed25519 CpJBgQ pFnL2XmxzppshipadVltN/zSgiRiMh6emu6O8EZTpxI
+K/RPjooKVSwqxc2aAUBtdTnkKoZvXDi+2NPB2NPXT9E
+-> ssh-ed25519 aXKGcg sTN4w5iMnwxmp/E7OKu5I3pUc695OXBYmfOY8/hs1AM
+DguaArDGVn7scD0NrDntgePjN1LFlfrPKfjEd1T9iOI
+-> ssh-ed25519 OkGqLg xuRTDdql+UBNW2go+XxkC/FJZa+N/e6Kj/Fjm7MzG3E
+KC39o7+WV+d/psN4mYSxeUSHsSCxPWTJgYjY1f1Dd3w
+-> J:e-grease
+CISPWfdtr4GKDU+lhCFk6B/EVyOmYwDxhChu
+--- nwu3QYk6rfvIJWJrTB8RSBsWjS1uok8rSxc9FCzoA9k
+WSMr
g#MSB}A"֞w}-Z1ooGo8Ҩw
\ No newline at end of file
diff --git a/ops/secrets/default.nix b/ops/secrets/default.nix
new file mode 100644
index 000000000000..43f2a738bb6b
--- /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/depot-inbox-imap.age b/ops/secrets/depot-inbox-imap.age
new file mode 100644
index 000000000000..9bce1845cb88
--- /dev/null
+++ b/ops/secrets/depot-inbox-imap.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw cpeIOVtFcfaHZpIAp495fkQLJoT++h1v6p0crBeuzFM
++zomKCg7UVNl/FlfcZflVPbo48C45uGoGoR1tbetEdk
+-> ssh-ed25519 zcCuhA loSmQUCnO0EBaGg+wFYYkXOdLBQ6Z+pPl4Y3oGx6xzw
++RdXNYYtIDDXGr1Z0Mh28psvF9gzg12M3EJTUqmdFtU
+-> ssh-ed25519 CpJBgQ 0W0LWu8WW6pQzUhK21CeNDUtW0srwR5gNCRjwTy94B4
+A02F+AyP+DajnVTJakx+0jynYRDix9I/9uZUDPjXpis
+-> ssh-ed25519 aXKGcg SVBo2urAYGSYrlj3ieoi9nkrffcZ9ZroCn86pZkn4nI
+xQRrLNeNcI9cpQY+X2xfLDoBqLNQixGjaYtMDWtHio4
+-> ssh-ed25519 BXptmQ UKNJPPjIiqPQndZ6/yASSg+5PQIn2N9nUy2hQMREq1Y
+X9zM/ji9R3jLOEDGLpIVESjU13VU0e3cTAR1xEMhY5I
+-> B-grease Y
+vUOYknqY0okoUOKZD/8MpnpwkOU31sszuUZfeSVsuVyUMPEbFjWQT74
+--- ymKMaoUQXFPRc9U0ZvULBEC0Az0ew2oEyHwH/kR9ETI
+Eu	xe_)zPhsGBLQ
\ No newline at end of file
diff --git a/ops/secrets/depot-replica-key.age b/ops/secrets/depot-replica-key.age
new file mode 100644
index 000000000000..5e8ce94d5d61
--- /dev/null
+++ b/ops/secrets/depot-replica-key.age
Binary files differdiff --git a/ops/secrets/gerrit-queue.age b/ops/secrets/gerrit-queue.age
new file mode 100644
index 000000000000..2e04be952d55
--- /dev/null
+++ b/ops/secrets/gerrit-queue.age
Binary files differdiff --git a/ops/secrets/gerrit-secrets.age b/ops/secrets/gerrit-secrets.age
new file mode 100644
index 000000000000..9ad123d578d4
--- /dev/null
+++ b/ops/secrets/gerrit-secrets.age
Binary files differdiff --git a/ops/secrets/grafana.age b/ops/secrets/grafana.age
new file mode 100644
index 000000000000..eef349d64c09
--- /dev/null
+++ b/ops/secrets/grafana.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw 0h55HIHm0kf6LqtI99LFUWBCoERBmpoF+anfnxjhDBU
+0bHlgfRABn51BoMwAIjUlaVnCr3ZDXkQPmFOiIV3TvI
+-> ssh-ed25519 zcCuhA 0vFMP1qFEiN4MUt+1qQCqtEovmO2d6QHj+KjHBrvqB4
+CUM2MDNPEKpksyCQmfDg/k/CKz7/ckgafw4aj0FLcmE
+-> ssh-ed25519 CpJBgQ Y971kTqyElTHpOw4D7mUfkIQFWELOBeuGPUE6bqSrXQ
+zt3ju2cqDfQJg9BsSsWcOGfPu5Q4XuIz0k2gasaRCPE
+-> ssh-ed25519 aXKGcg eNxh3cCMbxG/u4luhlE2WQVzFMlZIcDKDx4dcpK43hY
+HGJZYkWbYA0I7HtArCz9ErXwAAfOBHe20JH1J5Bx904
+-> ssh-ed25519 OkGqLg a1+l3dkThz8LLp7C1D9l7CzdB8Q4hxjNzaY7B6HMSnQ
+du3nw0b61TGdF91Mq7C/PpjDlnIIph1dVEIivcDpM7M
+-> \gwpw]-grease p#:x#sA ^S5*A/ ZpY
+1rTU2Rc5MnpJj8zwOK4yR9HvDPOiKjCKHOURq6ak4SUmEgqqyqoujzRaL4I0cKf0
+zMFTkoKnLXjjLiHyvJWqCGwCRq9veUsTiJ6jqs+y6L+YaT71qDzDXi3YfX2p
+--- hraNRaUxkHCnhk6AC/3jyxaAj1gyyIi0Q7cqoupcRrA
+:'!37 s+0@ׯd?!%lش͎;2B!/g/:wuՉ[~pF
\ No newline at end of file
diff --git a/ops/secrets/irccat.age b/ops/secrets/irccat.age
new file mode 100644
index 000000000000..2002b15c4957
--- /dev/null
+++ b/ops/secrets/irccat.age
Binary files differdiff --git a/ops/secrets/journaldriver.age b/ops/secrets/journaldriver.age
new file mode 100644
index 000000000000..c58773f36b21
--- /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 000000000000..54194df18383
--- /dev/null
+++ b/ops/secrets/keycloak-db.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw tWBrwZf6FNYAHRjoVV9/X6gJCXPqxZSoA01dvIrIOzg
+6W2A3smrrosM3sJgl5CT9vkCWqVKR3SaSxWS2nnwKJU
+-> ssh-ed25519 zcCuhA IS0OcHfEfb01xe+FJUe1poruK+uuP0MaJpeoGYyVAFY
+eEzcEYcW4KoKZZUEH/ha1nn9NudeK9HgPRgmrCWMjug
+-> ssh-ed25519 CpJBgQ 4mjCHMHfnGu2bhANPBNmcrZQrKBcPgZU+ll8opmvGCk
+0+Vd6pRPovUcKa9i37JVU/DUeYAmJ9D88MR4flA8gY8
+-> ssh-ed25519 aXKGcg WGCgCoViKLqndC35OTaExqZlPBDRwXRBJFuS7fw8n3Q
+kUHunOUgIsxXmOzMCwUFF/0dYiae8YZGmgZaz8gXPJo
+-> ssh-ed25519 OkGqLg LLIDJkImcqMjwRitnGevcav5YjDwYsQ//elx7fgbCQ4
+EnYTppSr/GKug9T+bFLGxrxUnNiXD5ODhB75OcH/h24
+-> j@-grease @:arA
+8EFNz7i8N3gbZEMaQw
+--- RkHJIg9pif/R47lgqrZD/XgkTETxXWkwW9QnFFsmfOA
+o]~6+jn]l+K=ʽ	Zp9RzVg u2_
\ No newline at end of file
diff --git a/ops/secrets/mkSecrets.nix b/ops/secrets/mkSecrets.nix
new file mode 100644
index 000000000000..c99130835f15
--- /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 000000000000..0381fb12907f
--- /dev/null
+++ b/ops/secrets/nix-cache-priv.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw 0Pp+oYDW8qhXoui/ewFhOTP10+JNOMS5qw66SuVHsXg
+Usi+hC3pq8gzqp/taDJr2C+7fM1qxunhrngbyGrUMJ8
+-> ssh-ed25519 zcCuhA xO33hAmuSPrpYeZXX1saM6mPYL5M6biLtBrsxc73+is
+S/pyKMUvn7zjjL3uIy3AJCkag4HpoOTh5SMYx/ZJ+rU
+-> ssh-ed25519 CpJBgQ D1PyFsBzoKLMocbcQpy4PE7lFQGweoI7MJDuAzDRUhM
+9+7ofW8vB3ZdS4A9nU0Rq+c4AJQPTZ0Bo/R3z1FY3io
+-> ssh-ed25519 aXKGcg u7+l6RDdquEw0/e55x+Yx/W0+019qNsxJzR8DCkxwj0
+tseOgvoIQk5QG65IOqBg65n7ToFXTjHT+QhPT1/9PE0
+-> ssh-ed25519 OkGqLg Hsk569u9xxHWQZKNqqxpQbFaX4KDjS9VRqE808vh/kA
+kiaoD3XCcrqfYEbneU+L7b2yPHo6ioUhtpxI9uEVnJw
+-> a{7<M_-grease k~MV B{E[
+sc3e
+--- dvGSRVY+ZDyS4cLqY8yguVZraB/IZSPaexlGMKLvnlQ
+(wΗ/ӖW$|Qj창z^	]ݣm$%.xKKl[nO75wi>g#q4^k;\ehX"w` 9_Coqݷ9mtK5
\ No newline at end of file
diff --git a/ops/secrets/nix-cache-pub.age b/ops/secrets/nix-cache-pub.age
new file mode 100644
index 000000000000..ae06f49d69d7
--- /dev/null
+++ b/ops/secrets/nix-cache-pub.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw +jfxfM1YDu5CoYtFeRWtpkUQhmFWn/kNBYsBnie7BVg
+XxL9l87hXD0zCUEwbSR9OHSYgpOw89Km5iyxPPnVDGQ
+-> ssh-ed25519 zcCuhA VAoDkN2gwErUFE/59V4IF9PbSBSleOjt2gosvYnHxWg
+Pf6eh8EfAdATjZIkQfhhqOXuJXIdwIpybITcn+rcutI
+-> ssh-ed25519 CpJBgQ C6zIv78gu+wBeAjhmXANegSNqGHnugemXBPQcTimgxg
+80109g83Hk+smWuZkTIZJ6VFQqJ+LU1boWKQIH1AHjc
+-> ssh-ed25519 aXKGcg lPb+kGr0vuJkQO6VutAm4Yh1CVi/XfqNdGbAh/B7ZRk
+h4xb++7I9iv8208oqY0xLruA1r62mepISFcusczdbgs
+-> ssh-ed25519 OkGqLg aOHt9OR8JChtYpclkgn9wCFnlayFje7WsMGQb8AqChU
+3VRTDMUwFtDcoxGU/wiBzTvS0SB/xOpBG6s+ENvAXVE
+-> Kow$7|\-grease
+8OGnQnY7gm4vMJRXjnBogA0HRU7hqIxs2sErFc7sV1CUNkZlFjdK8tZomlNwshjc
+p18HgtjJnaGhSqg1LyP7cJAo/XnSwDYCeNna/6vdlKBR3JeuOGTmx1NIG/cGSg
+--- w+jJplb/J3av+UcltcFf4qSqHoQ8Ol8lH/fFB3051Gw
+qIe:1*`j8snHcy7õ(.xD_}%)P,D6SHU9딬08\'
\ 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 000000000000..f2ccf7e96d73
--- /dev/null
+++ b/ops/secrets/oauth2_proxy.age
Binary files differdiff --git a/ops/secrets/owothia.age b/ops/secrets/owothia.age
new file mode 100644
index 000000000000..177ee61383c9
--- /dev/null
+++ b/ops/secrets/owothia.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw 8XtdgZ++/ZqmK4j8CO8oiuskTxjvKhWDK7fet5hbqiM
+Fs4O1vFtQL1JamnuCMPLzfzRPb90nxfXB6OXkyCMoHo
+-> ssh-ed25519 zcCuhA 6PNsPMdRXM77ci+mBQNRxr1oMGDNdlQilpUB0Q5es28
+APw2L/0htM9U0fJ1IUthdkoem/UTM/6NNQrgn4Vmpcs
+-> ssh-ed25519 CpJBgQ ed00il0q23M+3KH6hf5fFPaXGUKcz03Bn01jSoKiB1U
+jEN0Dk2edJBQreAlNE11sx0cI5u1mfFDT11Ev0KJ+gs
+-> ssh-ed25519 aXKGcg NocBhG6QGlWDZhjsA6Sxvjv9Gs+3Pq5gcOqnVdiefBg
+HYnqBv0pdPz8bqgZ98VDfYFeKcFNeuJrlOsyWt551Sg
+-> ssh-ed25519 OkGqLg e0081m/IkQafXh1gAWUZ2glYG7bklCG/LaUy63rK6gc
+G2RNMxCxRnqocYhiq142T8EPZQD8cRHHs7AHKFrMLaU
+-> +J}@hPk-grease
+406BMfqUt/KjayTopj4dNa4owPZphR6AsBXPurJwU/zV9ipirfW3oEeaprdh4uLg
+RHO0bSZQV1uu1YmbXkuwMaVj1cVn2vsDPEv3xG2SRzMoEpAAKaFCBba8
+--- 5ncLI9pS25vz5CebIZjPPDQ5cHISlyRFF55rGgFQnnM
+$"PK&C\(G[(
DlǺ84P)BԼc\U:(
\ No newline at end of file
diff --git a/ops/secrets/panettone.age b/ops/secrets/panettone.age
new file mode 100644
index 000000000000..0be42dc0a779
--- /dev/null
+++ b/ops/secrets/panettone.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw zzUe0JqhICtd/kgZnXFpwaQ1Ma6nqy/hMWaOJpRHmDs
+4cR+OnWShG6MpB/u0yfsSxplEch7x7DbygfBiJGxOOs
+-> ssh-ed25519 zcCuhA 0RZEYC9IuazO9fROalwoOCIgc0j+rNBP3gw7SKG0yEw
+mPRhN0hvccEr1A9ihWAFMH4/24vpBKpxBVq4BKBMmYM
+-> ssh-ed25519 CpJBgQ VrmfTtTVxuQmpUxMxtXtCnr8pFyqwtdyLHdbzYrlKlM
+kHgEdPmoIOLnGuMF5F5Ol1yZWcactSE4OZI0BSmDN+g
+-> ssh-ed25519 aXKGcg On4jwgsH504ZjYRwfw5oAfIDk3wU0+xgd43ryAn9H0I
+fayzht1ZPPiFCjuYTdwVtJu2nOUg4wtp5IipOR4oJm8
+-> ssh-ed25519 OkGqLg mubp0xI0fvsKOAUaNaftFkHJ+bxgFHbgjn+A7sR8XVs
+X68Zr8HvC4/XPC0AFIA5f1SKu7NSR/23oeX8cW1qfis
+-> ?`-grease
+hOy2Rwvk6+vXpHWWA49Wp10wKbw9TfsLXw
+--- 9MLGx6BVm40C0CSV3bq6dnXrpy3QunBlh2/uO5OisUU
+dzG<YAVs/-%g.e@,ZFW收&<Oq@>w̛Q>-gǓ'`XҟP8x<RNv9#'/)gm2v<,7邢qvQAO-ژ+gc#޵*e -) ;
\ No newline at end of file
diff --git a/ops/secrets/secrets.nix b/ops/secrets/secrets.nix
new file mode 100644
index 000000000000..3f7740a2e548
--- /dev/null
+++ b/ops/secrets/secrets.nix
@@ -0,0 +1,55 @@
+let
+  flokli = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPTVTXOutUZZjXLB0lUSgeKcSY/8mxKkC0ingGK1whD2 flokli"
+  ];
+
+  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";
+
+  terraform.publicKeys = tazjin ++ grfn ++ sterni ++ flokli;
+  whitbyDefault.publicKeys = tazjin ++ grfn ++ sterni ++ [ whitby ];
+  allDefault.publicKeys = tazjin ++ grfn ++ sterni ++ [ sanduny whitby ];
+  sandunyDefault.publicKeys = tazjin ++ grfn ++ sterni ++ [ sanduny ];
+in
+{
+  "besadii.age" = whitbyDefault;
+  "buildkite-agent-token.age" = whitbyDefault;
+  "buildkite-graphql-token.age" = whitbyDefault;
+  "buildkite-ssh-private-key.age" = whitbyDefault;
+  "clbot-ssh.age" = whitbyDefault;
+  "clbot.age" = whitbyDefault;
+  "depot-inbox-imap.age" = sandunyDefault;
+  "depot-replica-key.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-buildkite.age" = terraform;
+  "tf-glesys.age" = terraform;
+  "tf-keycloak.age" = terraform;
+  "tvl-alerts-bot-telegram-token.age" = whitbyDefault;
+}
diff --git a/ops/secrets/smtprelay.age b/ops/secrets/smtprelay.age
new file mode 100644
index 000000000000..62fbaffadf6c
--- /dev/null
+++ b/ops/secrets/smtprelay.age
@@ -0,0 +1,16 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw CW2Lgm0tSWUDwKSNSX/aLkVzQ/QeEeQgU3NITpz2D0M
+F7dA+zWdCz21s443bj9zCz6lBsRlFIxiG+l8CdbuPFk
+-> ssh-ed25519 zcCuhA l8rsBoYDwhUB5stbeGXYTQ4Fz745ywXFCOQZn2cMBW0
+TycVcUZjR2TDv5DPC54+RwoU6Fj4QpRUJj1j0HM/JCE
+-> ssh-ed25519 CpJBgQ CbwZO5LmSxd0HRYkf+lV+ymFcXSn/49GAPHG4l1I7gw
+xSmab5+BnAZF/B0n32xX1qZPdHgfoEMGIuZqlpnISjc
+-> ssh-ed25519 aXKGcg Tr+odf9p1RBrQK1guR6ToeN4wG1KLA3jwiPIkgyEjws
+TaeCnjiRp8VZoMS5qs+OfVbBc6zudayD693h/eGvVOo
+-> ssh-ed25519 OkGqLg Dmnsqz6PKzMd6w4t+l6+EWuia+stPwSEtu00KVuAojo
+rZ/i1WJhrCM/ZQTAroRRSjzUVJw2UJlPUe1uHYqSscw
+-> w!^Z-grease i86O2 i0.Rch
+/zsRadAGYzAY6F/J5m6lMjmojkN7NbY3TbfQbA
+--- /rQgwuY9SVGLKeUzY5P6c+sGQ1I1aw5cQxmO46QKDSQ
+(`U ,c|ґP畠9@&	gMߒ
+CH3ik3#|gMAgAnZYt2K2ޘY
\ No newline at end of file
diff --git a/ops/secrets/tf-buildkite.age b/ops/secrets/tf-buildkite.age
new file mode 100644
index 000000000000..0cf6066fa604
--- /dev/null
+++ b/ops/secrets/tf-buildkite.age
Binary files differdiff --git a/ops/secrets/tf-glesys.age b/ops/secrets/tf-glesys.age
new file mode 100644
index 000000000000..4e50454b6214
--- /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 000000000000..237b9377bd79
--- /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 000000000000..e897fedc03f5
--- /dev/null
+++ b/ops/secrets/tvl-alerts-bot-telegram-token.age
@@ -0,0 +1,15 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw JGXCnhez0LnlUV8eOitxizmxw/gV+1taBRhNvwvVcms
+qsRTOpifnoc0eorFjd4UlP7O3hkRR3KjDUcImASK0jY
+-> ssh-ed25519 zcCuhA KUcyaHcmuqCGtJBzvc2UK17gRrjzuzIxll+TS9Q4nWs
+CAJ19ClA9Tqj1fcYySq+K9gdZe6Uv0toZLnhlovr3tM
+-> ssh-ed25519 CpJBgQ OAE+u9JuC6KoefjCOTj4NkQElZRe6/EEIAGBN/XelnU
+M9MHlKxbEBJ+gACo2FiYqmm1cAoYW31+nP16qnVZ7Zw
+-> ssh-ed25519 aXKGcg Ll6v6v5HpUIEuOzjpVsPMmPQMnNkmyB4fz/YwNXfCHU
+MmFQy2WkKn5SM0bhe4NNe/lMnneKoOF+Ufq0t0QjNbw
+-> ssh-ed25519 OkGqLg PS6KLwat1z2BSQ9sIKDaryVU39EJR+iiAaKSP/KSPk0
+qUQP2f4MFk83zQ9edlSNC8jwpJvmp2xhOysd8rnYzW4
+-> >NI-grease @mOcHT z|%,s- mw^c *
+zu0M2pS6v3zehnLg
+--- jltBYy9brAtpkEIqPoGmIVe3s5XnWtpa9EmuXlAf91c
+tdX2-"#1n'\'{Dlw;Pִ@{B	!y+xWB:wtqph
\ No newline at end of file
diff --git a/ops/terraform/README.md b/ops/terraform/README.md
new file mode 100644
index 000000000000..9ff6c23d4701
--- /dev/null
+++ b/ops/terraform/README.md
@@ -0,0 +1,5 @@
+//ops/terraform
+===============
+
+This folder contains Terraform modules and other related
+Terraform-tooling by TVL.
diff --git a/ops/terraform/deploy-nixos/README.md b/ops/terraform/deploy-nixos/README.md
new file mode 100644
index 000000000000..ae876b45b93a
--- /dev/null
+++ b/ops/terraform/deploy-nixos/README.md
@@ -0,0 +1,44 @@
+deploy-nixos
+============
+
+This is a Terraform module to deploy a NixOS system closure to a
+remote machine.
+
+The system closure must be accessible by Nix-importing the repository
+root and building a specific attribute
+(e.g. `nix-build -A ops.machines.machine-name`).
+
+The target machine must be accessible normally over SSH, and an SSH
+key must be used for access.
+
+Notably this module separates the evaluation of the system closure from building
+and deploying it, and uses the closure's derivation hash to determine whether a
+deploy is necessary.
+
+## Usage example:
+
+```terraform
+module "deploy_somehost" {
+  source              = "git::https://code.tvl.fyi/depot.git:/ops/terraform/deploy-nixos.git"
+  attrpath            = "ops.nixos.somehost"
+  target_host         = "somehost.tvl.su"
+  target_user         = "someone"
+  target_user_ssh_key = tls_private_key.somehost.private_key_pem
+}
+```
+
+## Future work
+
+Several things can be improved about this module, for example:
+
+* The repository root (relative to which the attribute path is evaluated) could
+  be made configurable.
+
+* The remote system closure could be discovered to restore remote system state
+  after manual deploys on the target (i.e. "stomping" of changes).
+
+More ideas and contributions are, of course, welcome.
+
+## Acknowledgements
+
+Development of this module was sponsored by [Resoptima](https://resoptima.com/).
diff --git a/ops/terraform/deploy-nixos/main.tf b/ops/terraform/deploy-nixos/main.tf
new file mode 100644
index 000000000000..e07e9eb95614
--- /dev/null
+++ b/ops/terraform/deploy-nixos/main.tf
@@ -0,0 +1,109 @@
+# This module deploys a NixOS host by building a system closure
+# located at the specified attribute in the current repository.
+#
+# The closure's derivation path is persisted in the Terraform state to
+# determine after Nix evaluation whether the system closure has
+# changed and needs to be built/deployed.
+#
+# The system configuration is then built (or substituted) on the
+# machine that runs `terraform apply`, then copied and activated on
+# the target machine using `nix-copy-closure`.
+
+variable "attrpath" {
+  description = "attribute set path pointing to the NixOS system closure"
+  type        = string
+}
+
+variable "target_host" {
+  description = "address (IP or hostname) at which the target is reachable"
+  type        = string
+}
+
+variable "entrypoint" {
+  description = <<EOT
+    Path to a .nix file (or directory containing `default.nix` file)
+    that provides the attrset specified in `closure`.
+    If unset, asks git for the root of the repository.
+  EOT
+  type        = string
+  default     = ""
+}
+
+variable "target_user" {
+  description = "username on the target machine"
+  type        = string
+}
+
+variable "target_user_ssh_key" {
+  description = "SSH key to use for connecting to the target"
+  type        = string
+  default     = ""
+  sensitive   = true
+}
+
+variable "triggers" {
+  type        = map(string)
+  description = "Triggers for deploy"
+  default     = {}
+}
+
+# Fetch the derivation hash for the NixOS system.
+data "external" "nixos_system" {
+  program = ["${path.module}/nixos-eval.sh"]
+
+  query = {
+    attrpath   = var.attrpath
+    entrypoint = var.entrypoint
+  }
+}
+
+# Deploy the NixOS configuration if anything changed.
+resource "null_resource" "nixos_deploy" {
+  connection {
+    type        = "ssh"
+    host        = var.target_host
+    user        = var.target_user
+    private_key = var.target_user_ssh_key
+  }
+
+  # 1. Wait for SSH to become available.
+  provisioner "remote-exec" {
+    inline = ["true"]
+  }
+
+  # 2. Build NixOS system.
+  provisioner "local-exec" {
+    command = "nix-build ${data.external.nixos_system.result.drv} --no-out-link"
+  }
+
+  # 3. Copy closure to the target.
+  provisioner "local-exec" {
+    command = "${path.module}/nixos-copy.sh"
+
+    environment = {
+      SYSTEM_DRV  = data.external.nixos_system.result.drv
+      TARGET_HOST = var.target_host
+      DEPLOY_KEY  = var.target_user_ssh_key
+      TARGET_USER = var.target_user
+    }
+  }
+
+  # 4. Activate closure on the target.
+  provisioner "remote-exec" {
+    inline = [
+      "set -eu",
+      "SYSTEM=$(nix-build ${data.external.nixos_system.result.drv} --no-out-link)",
+      "sudo nix-env --profile /nix/var/nix/profiles/system --set $SYSTEM",
+      "sudo $SYSTEM/bin/switch-to-configuration switch",
+    ]
+  }
+
+  triggers = merge({
+    nixos_drv   = data.external.nixos_system.result.drv
+    target_host = var.target_host
+  }, var.triggers)
+}
+
+output "nixos_drv" {
+  value = data.external.nixos_system.result
+}
diff --git a/ops/terraform/deploy-nixos/nixos-copy.sh b/ops/terraform/deploy-nixos/nixos-copy.sh
new file mode 100755
index 000000000000..449002bbe512
--- /dev/null
+++ b/ops/terraform/deploy-nixos/nixos-copy.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+#
+# Copies a NixOS system to a target host, using the provided key,
+# or whatever ambient key is configured if the key is not set.
+set -ueo pipefail
+
+export NIX_SSHOPTS="\
+    -o StrictHostKeyChecking=no\
+    -o UserKnownHostsFile=/dev/null\
+    -o GlobalKnownHostsFile=/dev/null"
+
+# If DEPLOY_KEY was passed, write it to $scratch/id_deploy
+if [ -n "${DEPLOY_KEY-}" ]; then
+  scratch="$(mktemp -d)"
+  trap 'rm -rf -- "${scratch}"' EXIT
+
+  echo -n "$DEPLOY_KEY" > $scratch/id_deploy
+  chmod 0600 $scratch/id_deploy
+  export NIX_SSHOPTS="$NIX_SSHOPTS -o IdentityFile=$scratch/id_deploy"
+fi
+
+nix-copy-closure \
+  --to ${TARGET_USER}@${TARGET_HOST} \
+  ${SYSTEM_DRV} \
+  --gzip \
+  --include-outputs \
+  --use-substitutes
diff --git a/ops/terraform/deploy-nixos/nixos-eval.sh b/ops/terraform/deploy-nixos/nixos-eval.sh
new file mode 100755
index 000000000000..38f036bba936
--- /dev/null
+++ b/ops/terraform/deploy-nixos/nixos-eval.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+#
+# Builds a NixOS system configuration at the given attribute path.
+set -ueo pipefail
+
+# Load input variables from Terraform. jq's @sh format takes care of
+# escaping.
+eval "$(jq -r '@sh "ATTRPATH=\(.attrpath) && ENTRYPOINT=\(.entrypoint)"')"
+
+# Evaluate the system derivation.
+[[ -z "$ENTRYPOINT" ]] && ENTRYPOINT=$(git rev-parse --show-toplevel)
+SYSTEM_DRV=$(nix-instantiate -A "${ATTRPATH}" "${ENTRYPOINT}")
+
+# Return system derivation back to Terraform.
+jq -n --arg drv "$SYSTEM_DRV" '{"drv":$drv}'
diff --git a/ops/users/default.nix b/ops/users/default.nix
new file mode 100644
index 000000000000..8eb16a221988
--- /dev/null
+++ b/ops/users/default.nix
@@ -0,0 +1,217 @@
+{ ... }:
+
+[
+  {
+    username = "aaqaishtyaq";
+    email = "aaqaishtyaq@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$IpWJeEYTYEsrgGBNQcnbWA$w4+gQmeJlhddeaHvmbpNa3hDVg1BkJESZSVAd2eSOs4";
+  }
+  {
+    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 = "ghuntley";
+    email = "ghuntley@ghuntley.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$ciCuQHeA7csqrFUv7+asgw$7GUC5fLJWWVoHP8DvpA+C1u4+iFdV2E311kwTFwGzaQ";
+  }
+  {
+    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 = "IslandUsurper";
+    email = "lyle@menteeth.us";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$rNSsa8aYU4qvxeFnADgW1g$Zu6B6Al2usRRNfAKhWXzCAfiTfV3XQb0W6Op5TYN1oI";
+  }
+  {
+    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 = "jfroche";
+    email = "jfroche@pyxel.be";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$kA19gDabD1Fjy82olcmnsA$TTbkpAc0WYaA4DT2vc7+NAGXhC4Os1tPqZVpHFkzecE";
+  }
+  {
+    username = "jrhahn";
+    email = "mail.jhahn@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$giiu99hS7CzfsDZgxMNvKg$JiZZnFxOGHZRlUziYd3TkEiUplMz7Emy8fXfyLawPS0";
+  }
+  {
+    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 = "noteed";
+    email = "noteed@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$rcLfF9xXysSx5sahVQLiMA$EgRgAVXn8+r2Csa3XgIHIEBf3hX4Y58pOHf2eDaBUnA";
+  }
+  {
+    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 = "talyz";
+    email = "kim.lindberger@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$KYgHYsxX/DZDhnxdkzn1/w$L2Yyc2lYAREZP0FD3iX57MB6gzoOCcVmCGDxIsUGAgk";
+  }
+  {
+    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";
+  }
+  {
+    username = "brainrake";
+    email = "martonboros@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$f4/ewdyRBQbClL4KzqypHg$6Ql/xkmfIr60Qp1XMaFherqhh4cekLIbsi7KMM6izfE";
+  }
+  {
+    username = "raitobezarius";
+    email = "tvl@lahfa.xyz";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$3NZTBbF5dZssAHC/ktcA/Q$AZxHGG0ycNMOkIxC/ONYbyhNxC9hb6cpWvnsNH8LWZk";
+  }
+  {
+    username = "hsjobeki";
+    email = "hsjobeki@gmail.com";
+    password = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$jez9eVa2v0BznIJMOhw+hw$wUbwCS+Bfcjjzr08saQE6NNTPWNXWWaxv+UtBCdYC2s";
+  }
+]
diff --git a/ops/yandex-cloud-rs/.gitignore b/ops/yandex-cloud-rs/.gitignore
new file mode 100644
index 000000000000..ab3f21a96e8b
--- /dev/null
+++ b/ops/yandex-cloud-rs/.gitignore
@@ -0,0 +1,5 @@
+target/
+result/
+# Ignore everything under src (except for lib.rs)
+src/*
+!src/lib.rs
diff --git a/ops/yandex-cloud-rs/Cargo.lock b/ops/yandex-cloud-rs/Cargo.lock
new file mode 100644
index 000000000000..0944acf4f71e
--- /dev/null
+++ b/ops/yandex-cloud-rs/Cargo.lock
@@ -0,0 +1,1394 @@
+# 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 = "anyhow"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "axum"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bumpalo"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
+
+[[package]]
+name = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "flate2"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[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.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+dependencies = [
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tokio-io-timeout",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+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 = "io-lifetimes"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+
+[[package]]
+name = "js-sys"
+version = "0.3.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790"
+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.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "matchit"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
+
+[[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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "multimap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "petgraph"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+]
+
+[[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 = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools",
+ "lazy_static",
+ "log",
+ "multimap",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 1.0.109",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500"
+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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1a59b5d8e97dee33696bf13c5ba8ab85341c002922fba050069326b9c498974"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rustix"
+version = "0.37.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.100.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
+
+[[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 = "schannel"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
+dependencies = [
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.163"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2"
+
+[[package]]
+name = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "tempfile"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "tokio"
+version = "1.28.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-io-timeout"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
+dependencies = [
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "tonic"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64",
+ "bytes",
+ "flate2",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-timeout",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "rustls-native-certs",
+ "rustls-pemfile",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6fdaae4c2c638bb70fe42803a26fbd6fc6ac8c72f5c59f67ecc2a2dcabf4b07"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap",
+ "pin-project",
+ "pin-project-lite",
+ "rand",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "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.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.16",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
+
+[[package]]
+name = "web-sys"
+version = "0.3.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[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.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "yandex-cloud"
+version = "2023.5.23"
+dependencies = [
+ "prost",
+ "prost-types",
+ "tonic",
+ "tonic-build",
+ "walkdir",
+]
diff --git a/ops/yandex-cloud-rs/Cargo.toml b/ops/yandex-cloud-rs/Cargo.toml
new file mode 100644
index 000000000000..9bac344a67d0
--- /dev/null
+++ b/ops/yandex-cloud-rs/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "yandex-cloud"
+description = "Generated gRPC clients for the Yandex Cloud API"
+license = "MIT"
+version = "2023.5.23"
+edition = "2021"
+homepage = "https://cs.tvl.fyi/depot/-/tree/ops/yandex-cloud-rs"
+repository = "https://code.tvl.fyi/depot.git:/ops/yandex-cloud-rs.git"
+include = [ "/src", "README.md" ]
+
+[dependencies]
+prost = "0.11"
+prost-types = "0.11"
+
+[dependencies.tonic]
+version = "0.9"
+features = [ "tls", "tls-roots", "gzip" ]
+
+[build-dependencies]
+tonic-build = "0.9"
+walkdir = "2.3.3"
diff --git a/ops/yandex-cloud-rs/README.md b/ops/yandex-cloud-rs/README.md
new file mode 100644
index 000000000000..a80fa831637b
--- /dev/null
+++ b/ops/yandex-cloud-rs/README.md
@@ -0,0 +1,49 @@
+yandex-cloud-rs
+===============
+
+Client library for Yandex Cloud gRPC APIs, as published in their
+[GitHub repository][repo].
+
+Please see the [online documentation][docs] for user-facing
+information, this README is intended for library developers.
+
+The source code of the library lives [in the TVL repository][code].
+
+-------------
+
+In order to build this library, the gRPC API definitions need to be
+fetched from GitHub. By default this is done by Nix (see
+`default.nix`), which then injects the location of the API definitions
+through the `YANDEX_CLOUD_PROTOS` environment variable.
+
+The actual code generation happens through the calls in `build.rs`.
+
+Releases of this library are done from *dirty* trees, meaning that the
+version on crates.io should already contain all the generated code. In
+order to do this, after bumping the version in `Cargo.toml` and the
+API commit in `default.nix`, the following release procedure should be
+used:
+
+```
+# Get rid of all generated source files
+find src | grep '.rs$' | grep -v '^src/lib.rs$' | xargs rm
+
+# Get rid of all old artefacts
+cargo clean
+
+# Verify that a clean build works as intended
+cargo build
+
+# Verify that all documentation builds, and verify that it looks fine:
+#
+# - Is the version correct (current date)?
+# - Are all the services included (i.e. not an accidental empty build)?
+cargo doc --open
+
+# If everything looks fine, release:
+cargo publish --allow-dirty
+```
+
+[repo]: https://github.com/yandex-cloud/cloudapi
+[docs]: https://docs.rs/yandex-cloud/latest/yandex_cloud/
+[code]: https://cs.tvl.fyi/depot/-/tree/ops/yandex-cloud-rs
diff --git a/ops/yandex-cloud-rs/build.rs b/ops/yandex-cloud-rs/build.rs
new file mode 100644
index 000000000000..e9a96ef9df91
--- /dev/null
+++ b/ops/yandex-cloud-rs/build.rs
@@ -0,0 +1,43 @@
+use std::path::PathBuf;
+use walkdir::{DirEntry, WalkDir};
+
+fn proto_files(proto_dir: &str) -> Vec<PathBuf> {
+    let mut out = vec![];
+
+    fn is_proto(entry: &DirEntry) -> bool {
+        entry.file_type().is_file()
+            && entry
+                .path()
+                .extension()
+                .map(|e| e.to_string_lossy() == "proto")
+                .unwrap_or(false)
+    }
+
+    for entry in WalkDir::new(format!("{}/yandex", proto_dir)).into_iter() {
+        let entry = entry.expect("failed to list proto files");
+
+        if is_proto(&entry) {
+            out.push(entry.into_path())
+        }
+    }
+
+    out
+}
+
+fn main() {
+    if let Some(proto_dir) = option_env!("YANDEX_CLOUD_PROTOS") {
+        tonic_build::configure()
+            .build_client(true)
+            .build_server(false)
+            .out_dir("src/")
+            .include_file("includes.rs")
+            .compile(
+                &proto_files(proto_dir),
+                &[
+                    format!("{}", proto_dir),
+                    format!("{}/third_party/googleapis", proto_dir),
+                ],
+            )
+            .expect("failed to generate gRPC clients for Yandex Cloud")
+    }
+}
diff --git a/ops/yandex-cloud-rs/default.nix b/ops/yandex-cloud-rs/default.nix
new file mode 100644
index 000000000000..3c2f02a258f7
--- /dev/null
+++ b/ops/yandex-cloud-rs/default.nix
@@ -0,0 +1,22 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  protoSrc = pkgs.fetchFromGitHub {
+    owner = "yandex-cloud";
+    repo = "cloudapi";
+    rev = "d45e30883ea8a2186ed3375305eddc9c9c504c13";
+    sha256 = "01jcs3si4sllq685si8aby3g1lxjj46r1b7abmhak0m7xmgj0fb1";
+  };
+in
+pkgs.rustPlatform.buildRustPackage rec {
+  name = "yandex-cloud-rs";
+  src = depot.third_party.gitignoreSource ./.;
+  cargoLock.lockFile = ./Cargo.lock;
+  YANDEX_CLOUD_PROTOS = "${protoSrc}";
+  nativeBuildInputs = [ pkgs.protobuf ];
+
+  # The generated doc comments contain lots of things that rustc
+  # *thinks* are doctests, but are actually just garbage leading to
+  # compiler errors.
+  doCheck = false;
+}
diff --git a/ops/yandex-cloud-rs/examples/log-write.rs b/ops/yandex-cloud-rs/examples/log-write.rs
new file mode 100644
index 000000000000..84d183421ae9
--- /dev/null
+++ b/ops/yandex-cloud-rs/examples/log-write.rs
@@ -0,0 +1,37 @@
+//! This example uses the Yandex Cloud Logging API to write a log entry.
+
+use prost_types::Timestamp;
+use tonic::transport::channel::Endpoint;
+use yandex_cloud::yandex::cloud::logging::v1::destination::Destination;
+use yandex_cloud::yandex::cloud::logging::v1::log_ingestion_service_client::LogIngestionServiceClient;
+use yandex_cloud::yandex::cloud::logging::v1::Destination as OuterDestination;
+use yandex_cloud::yandex::cloud::logging::v1::IncomingLogEntry;
+use yandex_cloud::yandex::cloud::logging::v1::WriteRequest;
+use yandex_cloud::AuthInterceptor;
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let channel = Endpoint::from_static("https://ingester.logging.yandexcloud.net")
+        .connect()
+        .await?;
+
+    let mut client = LogIngestionServiceClient::with_interceptor(
+        channel,
+        AuthInterceptor::new("YOUR_TOKEN_HERE"),
+    );
+
+    let request = WriteRequest {
+        destination: Some(OuterDestination {
+            destination: Some(Destination::LogGroupId("YOUR_LOG_GROUP_ID".into())),
+        }),
+        entries: vec![IncomingLogEntry {
+            timestamp: Some(Timestamp::date_time(2023, 04, 24, 23, 44, 30).unwrap()),
+            message: "test log message".into(),
+            ..Default::default()
+        }],
+        ..Default::default()
+    };
+
+    client.write(request).await.unwrap();
+    Ok(())
+}
diff --git a/ops/yandex-cloud-rs/src/lib.rs b/ops/yandex-cloud-rs/src/lib.rs
new file mode 100644
index 000000000000..6b1425bc9e98
--- /dev/null
+++ b/ops/yandex-cloud-rs/src/lib.rs
@@ -0,0 +1,95 @@
+//! This module provides low-level generated gRPC clients for the
+//! Yandex Cloud APIs.
+//!
+//! The clients are generated using the [tonic][] and [prost][]
+//! crates and have default configuration.
+//!
+//! Documentation present in the protos is retained into the generated
+//! Rust types, but for detailed API information you should visit the
+//! official Yandex Cloud Documentation pages:
+//!
+//! * [in English](https://cloud.yandex.com/en-ru/docs/overview/api)
+//! * [in Russian](https://cloud.yandex.ru/docs/overview/api)
+//!
+//! The proto sources are available on the [Yandex Cloud GitHub][protos].
+//!
+//! [tonic]: https://docs.rs/tonic/latest/tonic/
+//! [prost]: https://docs.rs/prost/latest/prost/
+//! [protos]: https://github.com/yandex-cloud/cloudapi
+//!
+//! The majority of user-facing structures can be found in the
+//! [`yandex::cloud`] module.
+//!
+//! ## Usage
+//!
+//! Typically to use these APIs, you need to provide an authentication
+//! credential and an endpoint to connect to. The full list of
+//! Yandex's endpoints is [available online][endpoints] and you should
+//! look up the service you plan to use and pick the correct endpoint
+//! from the list.
+//!
+//! Authentication is done via an HTTP header using an IAM token,
+//! which can be done in Tonic using [interceptors][]. The
+//! [`AuthInterceptor`] provided by this crate can be used for that
+//! purpose.
+//!
+//! [endpoints]: https://cloud.yandex.com/en/docs/api-design-guide/concepts/endpoints
+//! [interceptors]: https://docs.rs/tonic/latest/tonic/service/trait.Interceptor.html
+
+use tonic::metadata::{Ascii, MetadataValue};
+use tonic::service::Interceptor;
+
+/// Helper trait for types or closures that can provide authentication
+/// tokens for Yandex Cloud.
+pub trait TokenProvider {
+    /// Fetch a currently valid authentication token for Yandex Cloud.
+    fn get_token<'a>(&'a mut self) -> &'a str;
+}
+
+impl TokenProvider for String {
+    fn get_token<'a>(&'a mut self) -> &'a str {
+        self.as_str()
+    }
+}
+
+impl TokenProvider for &'static str {
+    fn get_token(&mut self) -> &'static str {
+        *self
+    }
+}
+
+/// Interceptor for adding authentication headers to gRPC requests.
+/// This is constructed with a callable that returns authentication
+/// tokens.
+///
+/// This callable is responsible for ensuring that the returned tokens
+/// are valid at the given time, i.e. it should take care of
+/// refreshing and so on.
+pub struct AuthInterceptor<T: TokenProvider> {
+    token_provider: T,
+}
+
+impl<T: TokenProvider> AuthInterceptor<T> {
+    pub fn new(token_provider: T) -> Self {
+        Self { token_provider }
+    }
+}
+
+impl<T: TokenProvider> Interceptor for AuthInterceptor<T> {
+    fn call(
+        &mut self,
+        mut request: tonic::Request<()>,
+    ) -> Result<tonic::Request<()>, tonic::Status> {
+        let token: MetadataValue<Ascii> =
+            self.token_provider.get_token().try_into().map_err(|_| {
+                tonic::Status::invalid_argument("authorization token contained invalid characters")
+            })?;
+
+        request.metadata_mut().insert("authorization", token);
+
+        Ok(request)
+    }
+}
+
+// The rest of this file is generated by the build script at ../build.rs.
+include!("includes.rs");
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 000000000000..3a26366d4da6
--- /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 000000000000..267f23469775
--- /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 000000000000..f80dda512c5b
--- /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 000000000000..d2800bc9413a
--- /dev/null
+++ b/third_party/alsi/OWNERS
@@ -0,0 +1 @@
+grfn
diff --git a/third_party/alsi/default.nix b/third_party/alsi/default.nix
new file mode 100644
index 000000000000..8969374176d9
--- /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 000000000000..b03066ac060f
--- /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 000000000000..15af130916a3
--- /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 runCommand;
+in
+runCommand "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/cgit/.gitignore b/third_party/cgit/.gitignore
new file mode 100644
index 000000000000..661df346c24f
--- /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 000000000000..03b54796cf83
--- /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 000000000000..c108a7d34f2b
--- /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 000000000000..256ea6b3bc7d
--- /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 000000000000..d159169d1050
--- /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 000000000000..5967a327bb2e
--- /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 000000000000..2094b87df057
--- /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 000000000000..59372541cbc9
--- /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 000000000000..470da4fc1564
--- /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 000000000000..dd28a79315ac
--- /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 000000000000..7133a7ba373e
--- /dev/null
+++ b/third_party/cgit/cgit.css
@@ -0,0 +1,889 @@
+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.rev-deco {
+	color: #000;
+	padding: 0px 0.25em;
+	background-color: #eee;
+	border: solid 1px #aaa;
+}
+
+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.rev-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 000000000000..72fcd8498a07
--- /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 000000000000..5b9ed5be8e46
--- /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 000000000000..425528ee3951
--- /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 000000000000..cafb5355ea25
--- /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 000000000000..c664e894f788
--- /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 000000000000..6249b1d89243
--- /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 000000000000..e0391091e147
--- /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 000000000000..af7ca1973590
--- /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 000000000000..2f72ae9c0dd5
--- /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 000000000000..c783bda16ed2
--- /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 000000000000..190fb5501b49
--- /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 000000000000..85daf9c26b07
--- /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 000000000000..796ac308d237
--- /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 000000000000..012113c59132
--- /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 000000000000..0ef78841810e
--- /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 000000000000..59f43a84167f
--- /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 000000000000..02d90f81c9af
--- /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 000000000000..495eeceb279e
--- /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 000000000000..e912594c48a7
--- /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 000000000000..840bc34fffd4
--- /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 000000000000..80cf49af486b
--- /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 000000000000..ced781adc6b5
--- /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 000000000000..fa4de77587f2
--- /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 000000000000..e093aaf7019c
--- /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 000000000000..1b33266d534a
--- /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 000000000000..1e3b43debf5b
--- /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 000000000000..1afbd4bbcdbf
--- /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 000000000000..0bceb9891228
--- /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 000000000000..3fd2e965c8d2
--- /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 000000000000..65e111733803
--- /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 000000000000..da6f7a1b181b
--- /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 000000000000..31e7d5bb27b7
--- /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 000000000000..dd84fe3fcb94
--- /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 000000000000..ca08d69d142b
--- /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 000000000000..657765d897b5
--- /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 000000000000..82ef9b04e55e
--- /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 000000000000..b8864cb18791
--- /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 000000000000..bdf1435a16fe
--- /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 000000000000..2e140f5939c1
--- /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 000000000000..cfed1e7d6933
--- /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 000000000000..62a0a74a6473
--- /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 000000000000..0811ec407442
--- /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 000000000000..013d68024d5d
--- /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 000000000000..189ef2816620
--- /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 000000000000..66fa7d5d37e8
--- /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 000000000000..e5d357507cc3
--- /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 000000000000..dcdfbe532085
--- /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 000000000000..41f5041d62aa
--- /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 000000000000..0cf8441d4691
--- /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 000000000000..dda953bbf4ff
--- /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 000000000000..ca770994a6a6
--- /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 000000000000..5b97e035914e
--- /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 000000000000..c10ae42ebd05
--- /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 000000000000..16847b20b17a
--- /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 000000000000..5dccb639761a
--- /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 000000000000..3e460a3dbcf2
--- /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 000000000000..72cb98b72a3f
--- /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 000000000000..8198b4bacc36
--- /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 000000000000..2a64ae8f14af
--- /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 000000000000..39264a164fbc
--- /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 000000000000..520a7496a807
--- /dev/null
+++ b/third_party/cgit/ui-log.c
@@ -0,0 +1,564 @@
+/* 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 it is a depot revision, display it, otherwise
+			 * ... */
+			if (strncmp("refs/r/", buf, 7) == 0) {
+				html(" ");
+				cgit_log_link(/* trim 'refs/' */ buf + 5,
+					NULL, "rev-deco", buf, NULL,
+					ctx.qry.vpath, 0, NULL, NULL,
+					ctx.qry.showmsg, 0);
+			}
+
+			/* 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);
+		}
+	}
+	show_commit_decorations(commit);
+        html("&nbsp;");
+	cgit_commit_link(info->subject, NULL, NULL, ctx.qry.head,
+			 oid_to_hex(&commit->object.oid), ctx.qry.vpath);
+	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 000000000000..325607cdab62
--- /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 000000000000..4ac03cbef1d8
--- /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 000000000000..7a6cacd5acfe
--- /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 000000000000..65a205fad701
--- /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 000000000000..5bff07b83b99
--- /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 000000000000..456f610df41c
--- /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 000000000000..1d4a54a2c869
--- /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 000000000000..97b11c5f1678
--- /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 000000000000..1b6b3227dbc0
--- /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 000000000000..2e8896c982dd
--- /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 000000000000..6964873a6394
--- /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 000000000000..280139355ab0
--- /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 000000000000..a8deec3697b4
--- /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 000000000000..af8bc9e0659b
--- /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 000000000000..11f2714407c7
--- /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 000000000000..40ed6c21df62
--- /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 000000000000..0e61b03da3b3
--- /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 000000000000..ec7b76fabf24
--- /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 000000000000..cba696af53af
--- /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 000000000000..7be28081493f
--- /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 000000000000..d295cdcdbd59
--- /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 000000000000..21e0b88448a4
--- /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 000000000000..bbd34e3566a5
--- /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 000000000000..d2800bc9413a
--- /dev/null
+++ b/third_party/clj2nix/OWNERS
@@ -0,0 +1 @@
+grfn
diff --git a/third_party/clj2nix/default.nix b/third_party/clj2nix/default.nix
new file mode 100644
index 000000000000..f582debf29df
--- /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 000000000000..874aecd3e7b5
--- /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, localSystem, ... }:
+
+{
+  # 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 ? localSystem
+    , ...
+    }:
+    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 000000000000..0481d0940aa8
--- /dev/null
+++ b/third_party/elmPackages_0_18/default.nix
@@ -0,0 +1,21 @@
+# 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.
+#
+# Since the ABIs of current glibc and the pinned version have diverged
+# too much, we need to build //fun/gemma completely based on a historical
+# nixpkgs version.
+
+{ pkgs, ... }:
+
+(import
+  (pkgs.fetchFromGitHub {
+    owner = "NixOS";
+    repo = "nixpkgs";
+    rev = "14f9ee66e63077539252f8b4550049381a082518";
+    sha256 = "1wn7nmb1cqfk2j91l3rwc6yhimfkzxprb8wknw5wi57yhq9m6lv1";
+  })
+{ })
diff --git a/third_party/emacs/rcirc/default.nix b/third_party/emacs/rcirc/default.nix
new file mode 100644
index 000000000000..f34cbed7896a
--- /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 000000000000..b59cb4e9afe4
--- /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/.elpaignore b/third_party/exwm/.elpaignore
index b43bf86b50fd..b43bf86b50fd 100644
--- a/.elpaignore
+++ b/third_party/exwm/.elpaignore
diff --git a/third_party/exwm/.gitignore b/third_party/exwm/.gitignore
new file mode 100644
index 000000000000..9e4b0ee5b48e
--- /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 000000000000..9cecc1d4669e
--- /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 000000000000..6d7e0dd1ff17
--- /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/exwm-background.el b/third_party/exwm/exwm-background.el
index e7a0360c97c0..e7a0360c97c0 100644
--- a/exwm-background.el
+++ b/third_party/exwm/exwm-background.el
diff --git a/exwm-cm.el b/third_party/exwm/exwm-cm.el
index 8a4501003042..8a4501003042 100644
--- a/exwm-cm.el
+++ b/third_party/exwm/exwm-cm.el
diff --git a/exwm-config.el b/third_party/exwm/exwm-config.el
index 9609f4cf3ce0..9609f4cf3ce0 100644
--- a/exwm-config.el
+++ b/third_party/exwm/exwm-config.el
diff --git a/exwm-core.el b/third_party/exwm/exwm-core.el
index 75c7c1b17b77..75c7c1b17b77 100644
--- a/exwm-core.el
+++ b/third_party/exwm/exwm-core.el
diff --git a/exwm-floating.el b/third_party/exwm/exwm-floating.el
index 69e86a24e81b..69e86a24e81b 100644
--- a/exwm-floating.el
+++ b/third_party/exwm/exwm-floating.el
diff --git a/exwm-input.el b/third_party/exwm/exwm-input.el
index 79bc78ef0fe9..79bc78ef0fe9 100644
--- a/exwm-input.el
+++ b/third_party/exwm/exwm-input.el
diff --git a/exwm-layout.el b/third_party/exwm/exwm-layout.el
index ea186fe5a451..ea186fe5a451 100644
--- a/exwm-layout.el
+++ b/third_party/exwm/exwm-layout.el
diff --git a/exwm-manage.el b/third_party/exwm/exwm-manage.el
index c3d47f72259f..c3d47f72259f 100644
--- a/exwm-manage.el
+++ b/third_party/exwm/exwm-manage.el
diff --git a/exwm-randr.el b/third_party/exwm/exwm-randr.el
index 68bfdd70e949..68bfdd70e949 100644
--- a/exwm-randr.el
+++ b/third_party/exwm/exwm-randr.el
diff --git a/exwm-systemtray.el b/third_party/exwm/exwm-systemtray.el
index 0f199866241d..0f199866241d 100644
--- a/exwm-systemtray.el
+++ b/third_party/exwm/exwm-systemtray.el
diff --git a/exwm-workspace.el b/third_party/exwm/exwm-workspace.el
index 06217a77692a..06217a77692a 100644
--- a/exwm-workspace.el
+++ b/third_party/exwm/exwm-workspace.el
diff --git a/exwm-xim.el b/third_party/exwm/exwm-xim.el
index 9589648d22ca..9589648d22ca 100644
--- a/exwm-xim.el
+++ b/third_party/exwm/exwm-xim.el
diff --git a/exwm.el b/third_party/exwm/exwm.el
index 345f76beb66c..345f76beb66c 100644
--- a/exwm.el
+++ b/third_party/exwm/exwm.el
diff --git a/xinitrc b/third_party/exwm/xinitrc
index 591e4199144f..591e4199144f 100644
--- a/xinitrc
+++ b/third_party/exwm/xinitrc
diff --git a/third_party/gerrit-queue/.buildkite/build.sh b/third_party/gerrit-queue/.buildkite/build.sh
new file mode 100755
index 000000000000..0a218c817ee5
--- /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 000000000000..0885cb9694ee
--- /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 000000000000..f2ec770e3c34
--- /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 000000000000..261eeb9e9f8b
--- /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 000000000000..9ffb81b8d26d
--- /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 000000000000..d3988f59d125
--- /dev/null
+++ b/third_party/gerrit-queue/default.nix
@@ -0,0 +1,14 @@
+{ pkgs, lib, ... }:
+
+pkgs.buildGoModule {
+  pname = "gerrit-queue";
+  version = "master";
+  vendorHash = "sha256: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 000000000000..2cc65423f0e6
--- /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 000000000000..5d3997885c73
--- /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 000000000000..e04c0a349dfc
--- /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 000000000000..60f0c18113d7
--- /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 000000000000..f71032a567cb
--- /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 000000000000..314f97281c7e
--- /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 000000000000..788cf46f4ea6
--- /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 000000000000..295193ee9503
--- /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 000000000000..3929f8cf657e
--- /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 000000000000..d11545a6c9d5
--- /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 000000000000..eaa792c95828
--- /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 000000000000..3d4c5f3a837a
--- /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 000000000000..0b4cbcd8dd7c
--- /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-Syntax-highlight-nix.patch b/third_party/gerrit/0001-Syntax-highlight-nix.patch
new file mode 100644
index 000000000000..bdc3fd3b5510
--- /dev/null
+++ b/third_party/gerrit/0001-Syntax-highlight-nix.patch
@@ -0,0 +1,37 @@
+From 084e4f92fb58f7cd85303ba602fb3c40133c8fcc 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 1/3] Syntax highlight nix
+
+---
+ .../app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts     | 1 +
+ resources/com/google/gerrit/server/mime/mime-types.properties    | 1 +
+ 2 files changed, 2 insertions(+)
+
+diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+index a9f88bdd81..385249f280 100644
+--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
++++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+@@ -93,6 +93,7 @@ const LANGUAGE_MAP = new Map<string, string>([
+   ['text/x-vhdl', 'vhdl'],
+   ['text/x-yaml', 'yaml'],
+   ['text/vbscript', 'vbscript'],
++  ['text/x-nix', 'nix'],
+ ]);
+ 
+ const CLASS_PREFIX = 'gr-diff gr-syntax gr-syntax-';
+diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
+index 2f9561ba2e..739818ec05 100644
+--- a/resources/com/google/gerrit/server/mime/mime-types.properties
++++ b/resources/com/google/gerrit/server/mime/mime-types.properties
+@@ -149,6 +149,7 @@ mscin = text/x-mscgen
+ msgenny = text/x-msgenny
+ nb = text/x-mathematica
+ nginx.conf = text/x-nginx-conf
++nix = text/x-nix
+ nsh = text/x-nsis
+ nsi = text/x-nsis
+ nt = text/n-triples
+-- 
+2.37.3
+
diff --git a/third_party/gerrit/0002-Syntax-highlight-rules.pl.patch b/third_party/gerrit/0002-Syntax-highlight-rules.pl.patch
new file mode 100644
index 000000000000..4b91e2c3541f
--- /dev/null
+++ b/third_party/gerrit/0002-Syntax-highlight-rules.pl.patch
@@ -0,0 +1,37 @@
+From aedf8ac8fa5113843bcd83ff85e2d9f3bffdb16c 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 2/3] Syntax highlight rules.pl
+
+---
+ .../app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts     | 1 +
+ resources/com/google/gerrit/server/mime/mime-types.properties    | 1 +
+ 2 files changed, 2 insertions(+)
+
+diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+index 385249f280..7cb3068494 100644
+--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
++++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts
+@@ -68,6 +68,7 @@ const LANGUAGE_MAP = new Map<string, string>([
+   ['text/x-perl', 'perl'],
+   ['text/x-pgsql', 'pgsql'], // postgresql
+   ['text/x-php', 'php'],
++  ['text/x-prolog', 'prolog'],
+   ['text/x-properties', 'properties'],
+   ['text/x-protobuf', 'protobuf'],
+   ['text/x-puppet', 'puppet'],
+diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
+index 739818ec05..58eb727bf9 100644
+--- a/resources/com/google/gerrit/server/mime/mime-types.properties
++++ b/resources/com/google/gerrit/server/mime/mime-types.properties
+@@ -200,6 +200,7 @@ rq = application/sparql-query
+ rs = text/x-rustsrc
+ rss = application/xml
+ rst = text/x-rst
++rules.pl = text/x-prolog
+ README.md = text/x-gfm
+ s = text/x-gas
+ sas = text/x-sas
+-- 
+2.37.3
+
diff --git a/third_party/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch b/third_party/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch
new file mode 100644
index 000000000000..c4edee3a40c3
--- /dev/null
+++ b/third_party/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch
@@ -0,0 +1,215 @@
+From f49c50ca9a84ca374b7bd91c171bbea0457f2c7a 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 3/3] 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 |  5 +-
+ .../gerrit/httpd/raw/TitleComputer.java       | 67 +++++++++++++++++++
+ .../gerrit/httpd/raw/PolyGerritIndexHtml.soy  |  4 +-
+ 5 files changed, 89 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 72bfe40c3b..439bd73b44 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
++++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+@@ -41,6 +41,7 @@ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.HashSet;
+ import java.util.Map;
++import java.util.Optional;
+ import java.util.Set;
+ import java.util.function.Function;
+ 
+@@ -62,13 +63,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 = new HashSet<>();
+     enabledExperiments.addAll(experimentFeatures.getEnabledExperimentFeatures());
+     // Add all experiments enabled through url
+@@ -81,7 +83,8 @@ 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();
+@@ -129,6 +132,10 @@ public class IndexHtmlUtil {
+     }
+ 
+     data.put("gerritInitialData", initialData);
++
++    Optional<String> title = titleComputer.computeTitle(requestedURL);
++    title.ifPresent(s -> data.put("title", s));
++
+     return data.build();
+   }
+ 
+diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
+index fcb821e5ae..e1464b992b 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
++++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
+@@ -48,13 +48,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;
+@@ -69,6 +71,7 @@ public class IndexServlet extends HttpServlet {
+         (s) ->
+             UnsafeSanitizedContentOrdainer.ordainAsSafe(
+                 s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
++    this.titleComputer = titleComputer;
+   }
+ 
+   @Override
+@@ -86,7 +89,8 @@ public class IndexServlet extends HttpServlet {
+               faviconPath,
+               parameterMap,
+               urlOrdainer,
+-              getRequestUrl(req));
++              getRequestUrl(req),
++              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 15dcf42e0e..9f56bf33ce 100644
+--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
++++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
+@@ -241,10 +241,11 @@ public class StaticModule extends ServletModule {
+         @CanonicalWebUrl @Nullable String canonicalUrl,
+         @GerritServerConfig Config cfg,
+         GerritApi gerritApi,
+-        ExperimentFeatures experimentFeatures) {
++        ExperimentFeatures experimentFeatures,
++        TitleComputer titleComputer) {
+       String cdnPath = options.devCdn().orElse(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 dbfef44dfe..347ee75aab 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.37.3
+
diff --git a/third_party/gerrit/default.nix b/third_party/gerrit/default.nix
new file mode 100644
index 000000000000..94920aef8889
--- /dev/null
+++ b/third_party/gerrit/default.nix
@@ -0,0 +1,158 @@
+{ depot, pkgs, ... }:
+
+let
+  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_5.override { enableNixHacks = true; })
+      pkgs.jdk11_headless
+      pkgs.zlib
+      pkgs.python3
+      pkgs.curl
+      pkgs.nodejs
+      pkgs.yarn
+      pkgs.git
+      bazelRunScript
+    ];
+    runScript = "/bin/bazel-run";
+  };
+  bazel = bazelTop // { override = x: bazelTop; };
+  version = "3.7.0-rc4";
+in
+pkgs.lib.makeOverridable pkgs.buildBazelPackage {
+  pname = "gerrit";
+  inherit version;
+
+  src = pkgs.fetchgit {
+    url = "https://gerrit.googlesource.com/gerrit";
+    rev = "3e445c7833c4acf49f1171fe4c82ceb32e93c780";
+    branchName = "v${version}";
+    sha256 = "sha256:002aw2bfifyla66v8khyiq4m9qj6ahs6r1dzb5kjk8xqpf6c6q9p";
+    fetchSubmodules = true;
+  };
+
+  patches = [
+    ./0001-Syntax-highlight-nix.patch
+    ./0002-Syntax-highlight-rules.pl.patch
+    ./0003-Add-titles-to-CLs-over-HTTP.patch
+  ];
+
+  bazelTargets = [ "release" "api-skip-javadoc" ];
+  inherit bazel;
+
+  bazelFlags = [
+    "--repository_cache="
+    "--disk_cache="
+  ];
+
+  removeRulesCC = false;
+  fetchConfigured = true;
+
+  fetchAttrs = {
+    sha256 = "sha256:1bvcijcc083cbkzlc243bklnghnpj1knfnnbvvfhbmpmasm1svyn";
+    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:
+      # Normalize permissions on .yarn-{tarball,metadata} files
+      find $bazelOut/external/yarn_cache \( -name .yarn-tarball.tgz -or -name .yarn-metadata.json \) -exec chmod 644 {} +
+
+      mkdir $bazelOut/_bits/
+      find . -name node_modules -prune -print | while read d; do
+        echo "$d" "$(dirname $d)"
+        mkdir -p $bazelOut/_bits/$(dirname $d)
+        cp -R "$d" "$bazelOut/_bits/$(dirname $d)/node_modules"
+      done
+
+      (cd $bazelOut/ && tar czf $out --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner external/ _bits/)
+
+      runHook postInstall
+    '';
+  };
+
+  buildAttrs = {
+    preConfigure = ''
+      rm .bazelversion
+
+      cp -R $bazelOut/_bits/* ./
+    '';
+    postPatch = ''
+      # Disable all errorprone checks, since we might be using a different version.
+      sed -i \
+        -e '/-Xep:/d' \
+        -e '/-XepExcludedPaths:/a "-XepDisableAllChecks",' \
+        tools/BUILD
+    '';
+    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"
+    ];
+  };
+
+  meta.ci.targets = [ "deps" ];
+}
diff --git a/third_party/gerrit/detzip.go b/third_party/gerrit/detzip.go
new file mode 100644
index 000000000000..511c18ecfe17
--- /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 000000000000..50a4e78ae7cf
--- /dev/null
+++ b/third_party/gerrit_plugins/builder.nix
@@ -0,0 +1,39 @@
+{ depot, lib, pkgs, ... }:
+{
+  buildGerritBazelPlugin =
+    { name
+    , src
+    , depsOutputHash
+    , overlayPluginCmd ? ''
+        cp -R "${src}" "$out/plugins/${name}"
+      ''
+    , postPatch ? ""
+    , patches ? [ ]
+    }: ((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}
+      '';
+
+      bazelTargets = [ "//plugins/${name}" ];
+    }).overrideAttrs (super: {
+      deps = super.deps.overrideAttrs (superDeps: {
+        outputHash = depsOutputHash;
+      });
+      installPhase = ''
+        cp "bazel-bin/plugins/${name}/${name}.jar" "$out"
+      '';
+      postPatch = ''
+        ${super.postPatch or ""}
+        pushd "plugins/${name}"
+        ${lib.concatMapStringsSep "\n" (patch: ''
+          patch -p1 < ${patch}
+        '') patches}
+        popd
+        ${postPatch}
+      '';
+    }));
+}
diff --git a/third_party/gerrit_plugins/code-owners/default.nix b/third_party/gerrit_plugins/code-owners/default.nix
new file mode 100644
index 000000000000..3c0c4bed0fd7
--- /dev/null
+++ b/third_party/gerrit_plugins/code-owners/default.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, ... }@args:
+
+let
+  inherit (import ../builder.nix args) buildGerritBazelPlugin;
+in
+buildGerritBazelPlugin rec {
+  name = "code-owners";
+  depsOutputHash = "sha256:07mgvd7fvg1xqlabjn644505yx98vjrmwxx1arwsykir1h82h0b2";
+  src = pkgs.fetchgit {
+    url = "https://gerrit.googlesource.com/plugins/code-owners";
+    rev = "6fdf3ce2e52904b35e2a5824a4197155c2c6b4e4";
+    sha256 = "sha256:17k6310py71wax3881mf3vsf9zas648j4xzs9h0d7migv5nzsdzs";
+  };
+  patches = [
+    ./using-usernames.patch
+  ];
+}
diff --git a/third_party/gerrit_plugins/code-owners/using-usernames.patch b/third_party/gerrit_plugins/code-owners/using-usernames.patch
new file mode 100644
index 000000000000..7383aecc08c3
--- /dev/null
+++ b/third_party/gerrit_plugins/code-owners/using-usernames.patch
@@ -0,0 +1,481 @@
+From ba76ff8b7cd128383c86aeeacf12d1001670eec4 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Wed, 21 Sep 2022 03:15:38 +0100
+Subject: [PATCH] Add support for usernames and groups
+
+Change-Id: I3ba8527f66216d08e555a6ac4451fe0d1e090de5
+---
+ .../codeowners/backend/CodeOwnerResolver.java | 120 ++++++++++++++++--
+ .../FindOwnersCodeOwnerConfigParser.java      |   3 +-
+ ...AbstractFileBasedCodeOwnerBackendTest.java |   2 +-
+ .../backend/CodeOwnerResolverTest.java        |  87 ++++++++++++-
+ .../FindOwnersCodeOwnerConfigParserTest.java  |  32 ++++-
+ 5 files changed, 230 insertions(+), 14 deletions(-)
+
+diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+index 07894ced..40943659 100644
+--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
++++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java
+@@ -17,6 +17,8 @@ package com.google.gerrit.plugins.codeowners.backend;
+ import static com.google.common.base.Preconditions.checkState;
+ import static com.google.common.collect.ImmutableMap.toImmutableMap;
+ import static com.google.common.collect.ImmutableSet.toImmutableSet;
++import static com.google.common.collect.ImmutableSetMultimap.flatteningToImmutableSetMultimap;
++import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
+ import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError;
+ import static java.util.Objects.requireNonNull;
+ 
+@@ -25,6 +27,7 @@ import com.google.common.collect.ImmutableList;
+ import com.google.common.collect.ImmutableMap;
+ import com.google.common.collect.ImmutableMultimap;
+ import com.google.common.collect.ImmutableSet;
++import com.google.common.collect.ImmutableSetMultimap;
+ import com.google.common.collect.Iterables;
+ import com.google.common.collect.Streams;
+ import com.google.common.flogger.FluentLogger;
+@@ -33,17 +36,24 @@ import com.google.gerrit.entities.Project;
+ import com.google.gerrit.metrics.Timer0;
+ import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
+ import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics;
++import com.google.gerrit.server.AnonymousUser;
+ import com.google.gerrit.server.CurrentUser;
+ import com.google.gerrit.server.IdentifiedUser;
+ import com.google.gerrit.server.account.AccountCache;
+ import com.google.gerrit.server.account.AccountControl;
+ import com.google.gerrit.server.account.AccountState;
++import com.google.gerrit.server.account.GroupBackend;
++import com.google.gerrit.server.account.GroupBackends;
++import com.google.gerrit.server.account.InternalGroupBackend;
+ import com.google.gerrit.server.account.externalids.ExternalId;
+ import com.google.gerrit.server.account.externalids.ExternalIds;
+ import com.google.gerrit.server.permissions.GlobalPermission;
+ import com.google.gerrit.server.permissions.PermissionBackend;
+ import com.google.gerrit.server.permissions.PermissionBackendException;
++import com.google.gerrit.server.util.RequestContext;
++import com.google.gerrit.server.util.ThreadLocalRequestContext;
+ import com.google.inject.Inject;
++import com.google.inject.OutOfScopeException;
+ import com.google.inject.Provider;
+ import java.io.IOException;
+ import java.nio.file.Path;
+@@ -103,6 +113,8 @@ public class CodeOwnerResolver {
+ 
+   @VisibleForTesting public static final String ALL_USERS_WILDCARD = "*";
+ 
++  public static final String GROUP_PREFIX = "group:";
++
+   private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
+   private final PermissionBackend permissionBackend;
+   private final Provider<CurrentUser> currentUser;
+@@ -113,6 +125,8 @@ public class CodeOwnerResolver {
+   private final CodeOwnerMetrics codeOwnerMetrics;
+   private final UnresolvedImportFormatter unresolvedImportFormatter;
+   private final TransientCodeOwnerCache transientCodeOwnerCache;
++  private final InternalGroupBackend groupBackend;
++  private final ThreadLocalRequestContext context;
+ 
+   // Enforce visibility by default.
+   private boolean enforceVisibility = true;
+@@ -133,7 +147,9 @@ public class CodeOwnerResolver {
+       PathCodeOwners.Factory pathCodeOwnersFactory,
+       CodeOwnerMetrics codeOwnerMetrics,
+       UnresolvedImportFormatter unresolvedImportFormatter,
+-      TransientCodeOwnerCache transientCodeOwnerCache) {
++      TransientCodeOwnerCache transientCodeOwnerCache,
++      InternalGroupBackend groupBackend,
++      ThreadLocalRequestContext context) {
+     this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
+     this.permissionBackend = permissionBackend;
+     this.currentUser = currentUser;
+@@ -144,6 +160,8 @@ public class CodeOwnerResolver {
+     this.codeOwnerMetrics = codeOwnerMetrics;
+     this.unresolvedImportFormatter = unresolvedImportFormatter;
+     this.transientCodeOwnerCache = transientCodeOwnerCache;
++    this.groupBackend = groupBackend;
++    this.context = context;
+   }
+ 
+   /**
+@@ -357,6 +375,12 @@ public class CodeOwnerResolver {
+               "cannot resolve code owner email %s: no account with this email exists",
+               CodeOwnerResolver.ALL_USERS_WILDCARD));
+     }
++    if (codeOwnerReference.email().startsWith(GROUP_PREFIX)) {
++      return OptionalResultWithMessages.createEmpty(
++          String.format(
++              "cannot resolve code owner email %s: this is a group",
++              codeOwnerReference.email()));
++    }
+ 
+     ImmutableList.Builder<String> messageBuilder = ImmutableList.builder();
+     AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
+@@ -401,9 +425,53 @@ public class CodeOwnerResolver {
+       ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotations) {
+     requireNonNull(codeOwnerReferences, "codeOwnerReferences");
+ 
++    ImmutableSet<String> groupsToResolve =
++        codeOwnerReferences.stream()
++            .map(CodeOwnerReference::email)
++            .filter(ref -> ref.startsWith(GROUP_PREFIX))
++            .map(ref -> ref.substring(GROUP_PREFIX.length()))
++            .collect(toImmutableSet());
++
++    // When we call GroupBackends.findExactSuggestion we need to ensure that we
++    // have a user in context.  This is because the suggestion backend is
++    // likely to want to try to check that we can actually see the group it's
++    // returning (which we also check for explicitly, because I have trust
++    // issues).
++    RequestContext oldCtx = context.getContext();
++    // Check if we have a user in the context at all...
++    try {
++      oldCtx.getUser();
++    } catch (OutOfScopeException | NullPointerException e) {
++      // Nope.
++      RequestContext newCtx = () -> {
++        return new AnonymousUser();
++      };
++      context.setContext(newCtx);
++    }
++    ImmutableSetMultimap<String, CodeOwner> resolvedGroups = null;
++    try {
++      resolvedGroups =
++          groupsToResolve.stream()
++              .map(groupName -> GroupBackends.findExactSuggestion(groupBackend, groupName))
++              .filter(groupRef -> groupRef != null)
++              .filter(groupRef -> groupBackend.isVisibleToAll(groupRef.getUUID()))
++              .map(groupRef -> groupBackend.get(groupRef.getUUID()))
++              .collect(flatteningToImmutableSetMultimap(
++                    groupRef -> GROUP_PREFIX + groupRef.getName(),
++                    groupRef -> accountCache
++                        .get(ImmutableSet.copyOf(groupRef.getMembers()))
++                        .values().stream()
++                        .map(accountState -> CodeOwner.create(accountState.account().id()))));
++    } finally {
++      context.setContext(oldCtx);
++    }
++    ImmutableSetMultimap<CodeOwner, String> usersToGroups =
++        resolvedGroups.inverse();
++
+     ImmutableSet<String> emailsToResolve =
+         codeOwnerReferences.stream()
+             .map(CodeOwnerReference::email)
++            .filter(ref -> !ref.startsWith(GROUP_PREFIX))
+             .filter(filterOutAllUsersWildCard(ownedByAllUsers))
+             .collect(toImmutableSet());
+ 
+@@ -438,7 +506,8 @@ public class CodeOwnerResolver {
+     ImmutableMap<String, CodeOwner> codeOwnersByEmail =
+         accountsByEmail.map(mapToCodeOwner()).collect(toImmutableMap(Pair::key, Pair::value));
+ 
+-    if (codeOwnersByEmail.keySet().size() < emailsToResolve.size()) {
++    if (codeOwnersByEmail.keySet().size() < emailsToResolve.size() ||
++        resolvedGroups.keySet().size() < groupsToResolve.size()) {
+       hasUnresolvedCodeOwners.set(true);
+     }
+ 
+@@ -452,7 +521,9 @@ public class CodeOwnerResolver {
+         cachedCodeOwnersByEmail.entrySet().stream()
+             .filter(e -> e.getValue().isPresent())
+             .map(e -> Pair.of(e.getKey(), e.getValue().get()));
+-    Streams.concat(newlyResolvedCodeOwnersStream, cachedCodeOwnersStream)
++    Stream<Pair<String, CodeOwner>> resolvedGroupsCodeOwnersStream =
++        resolvedGroups.entries().stream().map(e -> Pair.of(e.getKey(), e.getValue()));
++    Streams.concat(Streams.concat(newlyResolvedCodeOwnersStream, cachedCodeOwnersStream), resolvedGroupsCodeOwnersStream)
+         .forEach(
+             p -> {
+               ImmutableSet.Builder<CodeOwnerAnnotation> annotationBuilder = ImmutableSet.builder();
+@@ -463,6 +534,12 @@ public class CodeOwnerResolver {
+               annotationBuilder.addAll(
+                   annotations.get(CodeOwnerReference.create(ALL_USERS_WILDCARD)));
+ 
++              // annotations for the groups this user is in apply as well
++              for (String group : usersToGroups.get(p.value())) {
++                annotationBuilder.addAll(
++                    annotations.get(CodeOwnerReference.create(group)));
++              }
++
+               if (!codeOwnersWithAnnotations.containsKey(p.value())) {
+                 codeOwnersWithAnnotations.put(p.value(), new HashSet<>());
+               }
+@@ -566,7 +643,7 @@ public class CodeOwnerResolver {
+     }
+ 
+     messages.add(String.format("email %s has no domain", email));
+-    return false;
++    return true;  // TVL: we allow domain-less strings which are treated as usernames.
+   }
+ 
+   /**
+@@ -581,11 +658,29 @@ public class CodeOwnerResolver {
+    */
+   private ImmutableMap<String, Collection<ExternalId>> lookupExternalIds(
+       ImmutableList.Builder<String> messages, ImmutableSet<String> emails) {
++    String[] actualEmails = emails.stream()
++      .filter(email -> email.contains("@"))
++      .toArray(String[]::new);
++    ImmutableSet<String> usernames = emails.stream()
++      .filter(email -> !email.contains("@"))
++      .collect(ImmutableSet.toImmutableSet());
+     try {
+-      ImmutableMap<String, Collection<ExternalId>> extIdsByEmail =
+-          externalIds.byEmails(emails.toArray(new String[0])).asMap();
++      ImmutableMap<String, Collection<ExternalId>> extIds =
++          new ImmutableMap.Builder<String, Collection<ExternalId>>()
++              .putAll(externalIds.byEmails(actualEmails).asMap())
++              .putAll(externalIds.allByAccount().entries().stream()
++                  .map(entry -> entry.getValue())
++                  .filter(externalId ->
++                      externalId.key().scheme() != null &&
++                      externalId.key().isScheme(ExternalId.SCHEME_USERNAME) &&
++                      usernames.contains(externalId.key().id()))
++                  .collect(toImmutableSetMultimap(
++                      externalId -> externalId.key().id(),
++                      externalId -> externalId))
++                  .asMap())
++              .build();
+       emails.stream()
+-          .filter(email -> !extIdsByEmail.containsKey(email))
++          .filter(email -> !extIds.containsKey(email))
+           .forEach(
+               email -> {
+                 transientCodeOwnerCache.cacheNonResolvable(email);
+@@ -594,7 +689,7 @@ public class CodeOwnerResolver {
+                         "cannot resolve code owner email %s: no account with this email exists",
+                         email));
+               });
+-      return extIdsByEmail;
++      return extIds;
+     } catch (IOException e) {
+       throw newInternalServerError(
+           String.format("cannot resolve code owner emails: %s", emails), e);
+@@ -811,6 +906,15 @@ public class CodeOwnerResolver {
+                 user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
+         return true;
+       }
++      if (!email.contains("@")) {
++        // the email is the username of the account, or a group, or something else.
++        messages.add(
++            String.format(
++                "account %s is visible to user %s",
++                accountState.account().id(),
++                user != null ? user.getLoggableName() : currentUser.get().getLoggableName()));
++        return true;
++      }
+ 
+       if (user != null) {
+         if (user.hasEmailAddress(email)) {
+diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
+index 5f350998..7977ba55 100644
+--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
++++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java
+@@ -149,7 +149,8 @@ public class FindOwnersCodeOwnerConfigParser implements CodeOwnerConfigParser {
+     private static final String EOL = "[\\s]*(#.*)?$"; // end-of-line
+     private static final String GLOB = "[^\\s,=]+"; // a file glob
+ 
+-    private static final String EMAIL_OR_STAR = "([^\\s<>@,]+@[^\\s<>@#,]+|\\*)";
++    // Also allows usernames, and group:$GROUP_NAME.
++    private static final String EMAIL_OR_STAR = "([^\\s<>@,]+@[^\\s<>@#,]+?|\\*|[a-zA-Z0-9_\\-]+|group:[a-zA-Z0-9_\\-]+)";
+     private static final String EMAIL_LIST =
+         "(" + EMAIL_OR_STAR + "(" + COMMA + EMAIL_OR_STAR + ")*)";
+ 
+diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
+index 7ec92959..59cf7e05 100644
+--- a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
++++ b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java
+@@ -424,7 +424,7 @@ public abstract class AbstractFileBasedCodeOwnerBackendTest extends AbstractCode
+               .commit()
+               .parent(head)
+               .message("Add invalid test code owner config")
+-              .add(JgitPath.of(codeOwnerConfigKey.filePath(getFileName())).get(), "INVALID"));
++              .add(JgitPath.of(codeOwnerConfigKey.filePath(getFileName())).get(), "INVALID!"));
+     }
+ 
+     // Try to update the code owner config.
+diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+index b32c3b5e..6b0f0cf8 100644
+--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
++++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java
+@@ -24,8 +24,10 @@ import com.google.gerrit.acceptance.TestAccount;
+ import com.google.gerrit.acceptance.TestMetricMaker;
+ import com.google.gerrit.acceptance.config.GerritConfig;
+ import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
++import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
+ import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+ import com.google.gerrit.entities.Account;
++import com.google.gerrit.entities.AccountGroup;
+ import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest;
+ import com.google.gerrit.server.ServerInitiated;
+ import com.google.gerrit.server.account.AccountsUpdate;
+@@ -51,6 +53,7 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest {
+   @Inject private RequestScopeOperations requestScopeOperations;
+   @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdate;
+   @Inject private AccountOperations accountOperations;
++  @Inject private GroupOperations groupOperations;
+   @Inject private ExternalIdNotes.Factory externalIdNotesFactory;
+   @Inject private TestMetricMaker testMetricMaker;
+   @Inject private ExternalIdFactory externalIdFactory;
+@@ -112,6 +115,18 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest {
+         .contains(String.format("account %s is visible to user %s", admin.id(), admin.username()));
+   }
+ 
++  @Test
++  public void resolveCodeOwnerReferenceForUsername() throws Exception {
++    OptionalResultWithMessages<CodeOwner> result =
++        codeOwnerResolverProvider
++            .get()
++            .resolveWithMessages(CodeOwnerReference.create(admin.username()));
++    assertThat(result.get()).hasAccountIdThat().isEqualTo(admin.id());
++    assertThat(result)
++        .hasMessagesThat()
++        .contains(String.format("account %s is visible to user %s", admin.id(), admin.username()));
++  }
++
+   @Test
+   public void cannotResolveCodeOwnerReferenceForStarAsEmail() throws Exception {
+     OptionalResultWithMessages<CodeOwner> result =
+@@ -127,6 +142,18 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest {
+                 CodeOwnerResolver.ALL_USERS_WILDCARD));
+   }
+ 
++  @Test
++  public void cannotResolveCodeOwnerReferenceForGroup() throws Exception {
++    OptionalResultWithMessages<CodeOwner> result =
++        codeOwnerResolverProvider
++            .get()
++            .resolveWithMessages(CodeOwnerReference.create("group:Administrators"));
++    assertThat(result).isEmpty();
++    assertThat(result)
++        .hasMessagesThat()
++        .contains("cannot resolve code owner email group:Administrators: this is a group");
++  }
++
+   @Test
+   public void resolveCodeOwnerReferenceForAmbiguousEmailIfOtherAccountIsInactive()
+       throws Exception {
+@@ -391,6 +418,64 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest {
+     assertThat(result.hasUnresolvedCodeOwners()).isFalse();
+   }
+ 
++  @Test
++  public void resolvePathCodeOwnersWhenNonVisibleGroupIsUsed() throws Exception {
++    CodeOwnerConfig codeOwnerConfig =
++        CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
++            .addCodeOwnerSet(
++                CodeOwnerSet.createWithoutPathExpressions("group:Administrators"))
++            .build();
++
++    CodeOwnerResolverResult result =
++        codeOwnerResolverProvider
++            .get()
++            .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
++    assertThat(result.codeOwnersAccountIds()).isEmpty();
++    assertThat(result.ownedByAllUsers()).isFalse();
++    assertThat(result.hasUnresolvedCodeOwners()).isTrue();
++  }
++
++  @Test
++  public void resolvePathCodeOwnersWhenVisibleGroupIsUsed() throws Exception {
++    AccountGroup.UUID createdGroupUUID = groupOperations
++        .newGroup()
++        .name("VisibleGroup")
++        .visibleToAll(true)
++        .addMember(admin.id())
++        .create();
++
++    CodeOwnerConfig codeOwnerConfig =
++        CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
++            .addCodeOwnerSet(
++                CodeOwnerSet.createWithoutPathExpressions("group:VisibleGroup"))
++            .build();
++
++    CodeOwnerResolverResult result =
++        codeOwnerResolverProvider
++            .get()
++            .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
++    assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id());
++    assertThat(result.ownedByAllUsers()).isFalse();
++    assertThat(result.hasUnresolvedCodeOwners()).isFalse();
++  }
++
++  @Test
++  public void resolvePathCodeOwnersWhenUsernameIsUsed() throws Exception {
++    CodeOwnerConfig codeOwnerConfig =
++        CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION)
++            .addCodeOwnerSet(
++                CodeOwnerSet.createWithoutPathExpressions(admin.username()))
++            .build();
++
++    CodeOwnerResolverResult result =
++        codeOwnerResolverProvider
++            .get()
++            .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md"));
++    assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id());
++    assertThat(result.ownedByAllUsers()).isFalse();
++    assertThat(result.hasUnresolvedCodeOwners()).isFalse();
++  }
++
+   @Test
+   public void resolvePathCodeOwnersNonResolvableCodeOwnersAreFilteredOut() throws Exception {
+     CodeOwnerConfig codeOwnerConfig =
+@@ -649,7 +734,7 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest {
+         "domain example.com of email foo@example.org@example.com is allowed");
+     assertIsEmailDomainAllowed(
+         "foo@example.org", false, "domain example.org of email foo@example.org is not allowed");
+-    assertIsEmailDomainAllowed("foo", false, "email foo has no domain");
++    assertIsEmailDomainAllowed("foo", true, "email foo has no domain");
+     assertIsEmailDomainAllowed(
+         "foo@example.com@example.org",
+         false,
+diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
+index 260e635e..7aab99d0 100644
+--- a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
++++ b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java
+@@ -158,16 +158,42 @@ public class FindOwnersCodeOwnerConfigParserTest extends AbstractCodeOwnerConfig
+                 codeOwnerConfigParser.parse(
+                     TEST_REVISION,
+                     CodeOwnerConfig.Key.create(project, "master", "/"),
+-                    getCodeOwnerConfig(EMAIL_1, "INVALID", "NOT_AN_EMAIL", EMAIL_2)));
++                    getCodeOwnerConfig(EMAIL_1, "INVALID!", "NOT!AN_EMAIL", EMAIL_2)));
+     assertThat(exception.getFullMessage(FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME))
+         .isEqualTo(
+             String.format(
+                 "invalid code owner config file '/OWNERS' (project = %s, branch = master):\n"
+-                    + "  invalid line: INVALID\n"
+-                    + "  invalid line: NOT_AN_EMAIL",
++                    + "  invalid line: INVALID!\n"
++                    + "  invalid line: NOT!AN_EMAIL",
+                 project));
+   }
+ 
++  @Test
++  public void codeOwnerConfigWithUsernames() throws Exception {
++    assertParseAndFormat(
++        getCodeOwnerConfig(EMAIL_1, "USERNAME", EMAIL_2),
++        codeOwnerConfig ->
++            assertThat(codeOwnerConfig)
++                .hasCodeOwnerSetsThat()
++                .onlyElement()
++                .hasCodeOwnersEmailsThat()
++                .containsExactly(EMAIL_1, "USERNAME", EMAIL_2),
++        getCodeOwnerConfig(EMAIL_1, "USERNAME", EMAIL_2));
++  }
++
++  @Test
++  public void codeOwnerConfigWithGroups() throws Exception {
++    assertParseAndFormat(
++        getCodeOwnerConfig(EMAIL_1, "group:tvl-employees", EMAIL_2),
++        codeOwnerConfig ->
++            assertThat(codeOwnerConfig)
++                .hasCodeOwnerSetsThat()
++                .onlyElement()
++                .hasCodeOwnersEmailsThat()
++                .containsExactly(EMAIL_1, "group:tvl-employees", EMAIL_2),
++        getCodeOwnerConfig(EMAIL_1, "group:tvl-employees", EMAIL_2));
++  }
++
+   @Test
+   public void codeOwnerConfigWithComment() throws Exception {
+     assertParseAndFormat(
+-- 
+2.37.3
+
diff --git a/third_party/gerrit_plugins/oauth/default.nix b/third_party/gerrit_plugins/oauth/default.nix
new file mode 100644
index 000000000000..39493ffb081d
--- /dev/null
+++ b/third_party/gerrit_plugins/oauth/default.nix
@@ -0,0 +1,19 @@
+{ depot, pkgs, ... }@args:
+
+let
+  inherit (import ../builder.nix args) buildGerritBazelPlugin;
+in
+buildGerritBazelPlugin rec {
+  name = "oauth";
+  depsOutputHash = "sha256:12y8493y8ns6c6c69g89bkrahd42mircqykldair1xrb3yg26hqp";
+  src = pkgs.fetchgit {
+    url = "https://gerrit.googlesource.com/plugins/oauth";
+    rev = "f9bef7476bc99f7b1dc3fe2d52ec95cd7ac571dc";
+    sha256 = "08wf50bz7ash37mzlrxfy7hvmjsf6s4ncpcw5969hs9hjvjfj4dz";
+  };
+  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"
+  '';
+}
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 000000000000..a249b1ce1bf2
--- /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 000000000000..eed07b5616c6
--- /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 000000000000..150de7c990e4
--- /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 000000000000..015d3e630879
--- /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 000000000000..c0e0335de734
--- /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 000000000000..e041edd4b698
--- /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 000000000000..8dc25bd918e0
--- /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 000000000000..d10332a1d572
--- /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 000000000000..3f451019e0fd
--- /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 000000000000..1395f3dce699
--- /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 000000000000..a858660f4335
--- /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 000000000000..c8426f2b1a2f
--- /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 000000000000..c2fc341feafd
--- /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 000000000000..119eafb42c15
--- /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 000000000000..191863bf0eab
--- /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 000000000000..63c6f4b1d764
--- /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 000000000000..8d540877d577
--- /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 000000000000..401fc6eb40e7
--- /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 000000000000..6b4e8f5275bc
--- /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 000000000000..decb7f3db944
--- /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 000000000000..6ba12afff763
--- /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 000000000000..3186a066291d
--- /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 000000000000..8c593eaae8c5
--- /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 000000000000..c7c50795c017
--- /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 000000000000..504d53595440
--- /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 000000000000..4588c1b589a5
--- /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 000000000000..f37d70bbda03
--- /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 000000000000..72fb96d475ee
--- /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 000000000000..210ab1bc70a2
--- /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 000000000000..078592aa9d5f
--- /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 000000000000..b1ee6da1fc32
--- /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 000000000000..41c3b4e7263c
--- /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 000000000000..9a8fef694857
--- /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 000000000000..60864ffe4a58
--- /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 000000000000..8da07693ce4e
--- /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 000000000000..f5a900b95837
--- /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 000000000000..1c03a8f0a98c
--- /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 000000000000..490d3a9d2b12
--- /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 000000000000..cba54e589060
--- /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 000000000000..522a27d60281
--- /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 000000000000..4d29cc2f8993
--- /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 000000000000..7bfe5500232c
--- /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 000000000000..b2773d85d518
--- /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 000000000000..ce5fe1d24073
--- /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 000000000000..1b4659d3d84d
--- /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 000000000000..a64022791462
--- /dev/null
+++ b/third_party/hii/OWNERS
@@ -0,0 +1 @@
+Profpatsch
diff --git a/third_party/irccat/default.nix b/third_party/irccat/default.nix
new file mode 100644
index 000000000000..c5d7a5f6dfe6
--- /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;
+  vendorHash = "sha256:06a985y4alw1rsghgmhfyczns6klz7bbkfn5mnqc9fdfclgg4s3r";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "irccloud";
+    repo = "irccat";
+    rev = "17451e7e267f099e9614ec945541b624520f607e";
+    sha256 = "0l99mycxymyslwi8mmyfdcqa8pdp79wcyb04s5j5y4grmlsxw1wx";
+  };
+}
diff --git a/third_party/josh/default.nix b/third_party/josh/default.nix
new file mode 100644
index 000000000000..45c8420e96e7
--- /dev/null
+++ b/third_party/josh/default.nix
@@ -0,0 +1,37 @@
+# https://github.com/josh-project/josh
+{ depot, pkgs, ... }:
+
+let
+  # TODO(sterni): switch to pkgs.josh as soon as that commit is released
+  rev = "fc857afda2c1536234e3bb1983c518a1abf63d25";
+  src = pkgs.fetchFromGitHub {
+    owner = "josh-project";
+    repo = "josh";
+    inherit rev;
+    hash = "sha256:16ch7al7xfyjipgqh2n7grj985fv713mhi8y5bixb736vsad9q3w";
+  };
+in
+depot.third_party.naersk.buildPackage {
+  inherit src;
+  JOSH_VERSION = "git-${builtins.substring 0 8 rev}";
+
+  buildInputs = with pkgs; [
+    libgit2
+    openssl
+    pkg-config
+  ];
+
+  cargoBuildOptions = x: x ++ [
+    "-p"
+    "josh-filter"
+    "-p"
+    "josh-proxy"
+  ];
+
+  overrideMain = x: {
+    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 000000000000..f753878f7cf8
--- /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 000000000000..d55b7bc362fe
--- /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 000000000000..8dd1b65b9bc4
--- /dev/null
+++ b/third_party/lisp/OWNERS
@@ -0,0 +1,2 @@
+eta
+grfn
diff --git a/third_party/lisp/alexandria.nix b/third_party/lisp/alexandria.nix
new file mode 100644
index 000000000000..b522e2d142c1
--- /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 000000000000..c079943e6725
--- /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 000000000000..2b45716e4709
--- /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 000000000000..bdf4ad2ae6dd
--- /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 000000000000..b4c74feefe82
--- /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 000000000000..7ccdd1888163
--- /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 000000000000..76c6845b82b3
--- /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 000000000000..e8ec4aa8f85c
--- /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 000000000000..41202746d019
--- /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 000000000000..1d7fb2bab43d
--- /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 000000000000..ae7c5dd23d18
--- /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 000000000000..8a2e09950887
--- /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 000000000000..de1d0c2e8ed7
--- /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 000000000000..59e9914ee1d1
--- /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 000000000000..d3f50bcb1af8
--- /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 000000000000..0e3401524755
--- /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 000000000000..08055a047119
--- /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 000000000000..b51e4d46a747
--- /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 000000000000..34201bc2faa0
--- /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 000000000000..e53cb2dfce42
--- /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 000000000000..9350abe2e3a3
--- /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 000000000000..6b82fac772d4
--- /dev/null
+++ b/third_party/lisp/cl-json.nix
@@ -0,0 +1,53 @@
+# JSON encoder & decoder
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix) buildLisp;
+
+  # https://github.com/sharplispers/cl-json/pull/12/
+  src = pkgs.fetchFromGitHub {
+    owner = "sternenseemann";
+    repo = "cl-json";
+    rev = "c059bec94e28a11102a994d6949e2e52764f21fd";
+    sha256 = "0l07syw1b1x2zi8kj4iph3rf6vi6c16b7fk69iv7x27wrdsr1qwj";
+  };
+
+  getSrcs = subdir: map (f: src + ("/" + subdir + "/" + f));
+in
+buildLisp.library {
+  name = "cl-json";
+  deps = [ (buildLisp.bundled "asdf") ];
+
+  srcs = [ "${src}/cl-json.asd" ] ++
+    (getSrcs "src" [
+      "package.lisp"
+      "common.lisp"
+      "objects.lisp"
+      "camel-case.lisp"
+      "decoder.lisp"
+      "encoder.lisp"
+      "utils.lisp"
+      "json-rpc.lisp"
+    ]);
+
+  tests = {
+    deps = [
+      depot.third_party.lisp.cl-unicode
+      depot.third_party.lisp.fiveam
+    ];
+    srcs = [
+      # CLOS tests are broken upstream as well
+      # https://github.com/sharplispers/cl-json/issues/11
+      (pkgs.writeText "no-clos-tests.lisp" ''
+        (replace *features* (delete :cl-json-clos *features*))
+      '')
+    ] ++ getSrcs "t" [
+      "package.lisp"
+      "testencoder.lisp"
+      "testdecoder.lisp"
+      "testmisc.lisp"
+    ];
+
+    expression = "(fiveam:run! 'json-test::json)";
+  };
+}
diff --git a/third_party/lisp/cl-plus-ssl.nix b/third_party/lisp/cl-plus-ssl.nix
new file mode 100644
index 000000000000..dc0a95944fe7
--- /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 000000000000..561e306191ca
--- /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 000000000000..188cbc686d28
--- /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 000000000000..7ab9bea59ff5
--- /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 000000000000..815d99c2dc8e
--- /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 000000000000..601b09f118db
--- /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 000000000000..b40d5d0601b2
--- /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 000000000000..145b9cfd4325
--- /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 000000000000..7f7f79f8551b
--- /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 000000000000..1886ea2ec9a2
--- /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 000000000000..a9ffd8085e89
--- /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 000000000000..ce7fb33abff1
--- /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 000000000000..c31ddb3c5b74
--- /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 000000000000..607f438d7e72
--- /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 000000000000..5caf8261fa1c
--- /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 000000000000..500e980a819a
--- /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 000000000000..a6a06d4ad057
--- /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 000000000000..a3d27a09b6a0
--- /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 000000000000..e2480cd349f1
--- /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 000000000000..324c5da265d8
--- /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 000000000000..b7d60265ac37
--- /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 000000000000..00f66c1fe314
--- /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 000000000000..bd7f31dfa0de
--- /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 000000000000..296112cc9e9a
--- /dev/null
+++ b/third_party/lisp/lisp-binary.nix
@@ -0,0 +1,33 @@
+# A library to easily read and write complex binary formats.
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.srcOnly pkgs.lispPackages.lisp-binary;
+in
+depot.nix.buildLisp.library {
+  name = "lisp-binary";
+
+  deps = with depot.third_party.lisp; [
+    alexandria
+    cffi
+    closer-mop
+    flexi-streams
+    moptilities
+    quasiquote_2
+  ];
+
+  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" # TODO(sterni): disable conditionally cffi for ECL
+  ];
+}
diff --git a/third_party/lisp/local-time.nix b/third_party/lisp/local-time.nix
new file mode 100644
index 000000000000..1358408d387c
--- /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 000000000000..73a1664a0162
--- /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 000000000000..8c3e255f166b
--- /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 000000000000..fc046d08951a
--- /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 000000000000..5051f60d6b86
--- /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 000000000000..2e9580706346
--- /dev/null
+++ b/third_party/lisp/mime4cl/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/third_party/lisp/mime4cl/README.md b/third_party/lisp/mime4cl/README.md
new file mode 100644
index 000000000000..2704d481ed3c
--- /dev/null
+++ b/third_party/lisp/mime4cl/README.md
@@ -0,0 +1,27 @@
+# mime4cl
+
+`MIME4CL` is a Common Lisp library for dealing with MIME messages. It was
+originally been written by Walter C. Pelissero and vendored into depot
+([mime4cl-20150207T211851.tbz](http://wcp.sdf-eu.org/software/mime4cl-20150207T211851.tbz)
+to be exact) as upstream has become inactive. Its [original
+website](http://wcp.sdf-eu.org/software/#mime4cl) can still be accessed.
+
+The depot version has since diverged from upstream. Main aims were to improve
+performance and reduce code size by relying on third party libraries like
+flexi-streams. It is planned to improve encoding handling in the long term.
+Currently, the library is being worked on intermittently and not very well
+tested—**it may not work as expected**.
+
+## Differences from the original version
+
+* `//nix/buildLisp` is used as the build system. ASDF is currently untested and
+  may be broken.
+
+* The dependency on [sclf](http://wcp.sdf-eu.org/software/#sclf) has been
+  eliminated by inlining the relevant parts.
+
+* `MY-STRING-INPUT-STREAM`, `DELIMITED-INPUT-STREAM`,
+  `CHARACTER-INPUT-ADAPTER-STREAM`, `BINARY-INPUT-ADAPTER-STREAM` etc. have been
+  replaced by (thin wrappers around) flexi-streams. In addition to improved
+  handling of encodings, this allows using `READ-SEQUENCE` via the gray stream
+  interface.
diff --git a/third_party/lisp/mime4cl/address.lisp b/third_party/lisp/mime4cl/address.lisp
new file mode 100644
index 000000000000..42688a595b26
--- /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-2023 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)
+  (let ((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)))))
+    (let ((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)
+    (let ((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."
+  (let ((grammar (force define-grammar)))
+    (with-input-from-string (stream string)
+      (let* ((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."
+  (let ((grammar (force define-grammar)))
+    (with-input-from-string (stream string)
+      (let ((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 000000000000..99b23c91aa69
--- /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.flexi-streams
+    depot.third_party.lisp.npg
+    depot.third_party.lisp.trivial-gray-streams
+    depot.third_party.lisp.qbase64
+  ];
+
+  srcs = [
+    ./ex-sclf.lisp
+    ./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)
+
+        ;; override auto discovery which doesn't work in the nix store
+        (defvar *samples-directory* (pathname "${./test/samples}/"))
+      '')
+      ./test/temp-file.lisp
+      ./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 000000000000..2e282c237822
--- /dev/null
+++ b/third_party/lisp/mime4cl/endec.lisp
@@ -0,0 +1,663 @@
+;;;  endec.lisp --- encoder/decoder functions
+
+;;;  Copyright (C) 2005-2008, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2023 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)
+
+(defun redirect-stream (in out &key (buffer-size 4096))
+  "Consume input stream IN and write all its content to output stream OUT.
+The streams' element types need to match."
+  (let ((buf (make-array 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))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; Thank you SBCL for rendering constants totally useless!
+(defparameter +base64-encode-table+
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
+
+(declaim (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 (let ((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 ()
+               (let ((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 #\=)
+                        (let ((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
+                                 (let ((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)
+    `(let ((,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."
+  (let ((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."
+  (let ((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)))
+
+(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."
+  ;; parser-errors are ignored for base64
+  (declare (ignore parser-errors))
+  (redirect-stream (make-instance 'qbase64:decode-stream
+                                  :underlying-stream in)
+                   out))
+
+(defun decode-base64-stream-to-sequence (stream &key parser-errors)
+  "Read Base64 characters from STREAM and return result of decoding them as a
+binary sequence."
+  ;; parser-errors are ignored for base64
+  (declare (ignore parser-errors))
+  (let* ((buffered-size 4096)
+         (dstream (make-instance 'qbase64:decode-stream
+                                 :underlying-stream stream))
+         (output-seq (make-array buffered-size
+                                 :element-type '(unsigned-byte 8)
+                                 :adjustable t)))
+    (loop for cap = (array-dimension output-seq 0)
+          for pos = (read-sequence output-seq dstream :start (or pos 0))
+          if (>= pos cap)
+            do (adjust-array output-seq (+ cap buffered-size))
+          else
+            do (progn
+                 (adjust-array output-seq pos)
+                 (return output-seq)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(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-string (string encoding &key parser-errors-p)
+  (gcase (encoding string-equal)
+    (:quoted-printable
+     (decode-quoted-printable-string string
+                                     :parser-errors parser-errors-p))
+    (:base64
+     ;; parser-errors-p is unused in base64
+     (qbase64:decode-string string))
+    (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" (qbase64:decode-string (subseq string start 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 flexi-streams, otherwise signal an error."
+  (flet ((decode-part (part)
+           (etypecase part
+             (cons (flexi-streams:octets-to-string
+                    (car part)
+                    :external-format (flexi-streams:make-external-format
+                                      (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/ex-sclf.lisp b/third_party/lisp/mime4cl/ex-sclf.lisp
new file mode 100644
index 000000000000..7951b44f4d0f
--- /dev/null
+++ b/third_party/lisp/mime4cl/ex-sclf.lisp
@@ -0,0 +1,368 @@
+;;; ex-sclf.lisp --- subset of sclf used by mime4cl
+
+;;;  Copyright (C) 2005-2010 by Walter C. Pelissero
+;;;  Copyright (C) 2022-2023 The TVL Authors
+
+;;;  Author: sternenseemann <sternenseemann@systemli.org>
+;;;  Project: mime4cl
+;;;
+;;;  mime4cl uses sclf for miscellaneous utility functions. sclf's portability
+;;;  is quite limited. Since mime4cl is the only thing in TVL's depot depending
+;;;  on sclf, it made more sense to strip down sclf to the extent mime4cl needed
+;;;  in order to lessen the burden of porting it to other CL implementations
+;;;  later.
+;;;
+;;;  Eventually it probably makes sense to drop the utilities we don't like and
+;;;  merge the ones we do like into depot's own utility package, klatre.
+
+#+cmu (ext:file-comment "$Module: ex-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
+
+(defpackage :mime4cl-ex-sclf
+  (:use :common-lisp)
+  (:export
+   #:aif
+   #:awhen
+   #:aand
+   #:it
+
+   #:gcase
+
+   #:with-gensyms
+
+   #:split-at
+   #:split-string-at-char
+   #:+whitespace+
+   #:whitespace-p
+   #:string-concat
+   #:s+
+   #:string-starts-with
+   #:string-trim-whitespace
+   #:string-left-trim-whitespace
+   #:string-right-trim-whitespace
+
+   #:queue
+   #:make-queue
+   #:queue-append
+   #:queue-pop
+   #:queue-empty-p
+
+   #:save-file-excursion
+   #:read-file
+
+   #:unix-file-stat
+   #:unix-stat
+   #:file-size
+
+   #:promise
+   #:make-promise
+   #:lazy
+   #:force
+   #:forced-p
+   #:deflazy
+
+   #:f++
+
+   #:week-day->string
+   #:month->string))
+
+(in-package :mime4cl-ex-sclf)
+
+;; MACRO UTILS
+
+(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))
+
+;; CONTROL FLOW
+
+(defmacro aif (test then &optional else)
+  `(let ((it ,test))
+     (if it
+         ,then
+         ,else)))
+
+(defmacro awhen (test &body then)
+  `(let ((it ,test))
+     (when it
+       ,@then)))
+
+(defmacro aand (&rest args)
+  (cond ((null args) t)
+        ((null (cdr args)) (car args))
+        (t `(aif ,(car args) (aand ,@(cdr args))))))
+
+(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)
+    `(let ((,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)))))
+
+;; SEQUENCES
+
+(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 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."
+  (let ((len (length sequence)))
+    (labels ((split-from (start)
+               (unless (>= start len)
+                 (let ((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))))
+
+;; STRINGS
+
+(defvar +whitespace+ '(#\return #\newline #\tab #\space #\page))
+
+(defun whitespace-p (char)
+  (member char +whitespace+))
+
+(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 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)
+             (let ((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 s+ (&rest strings)
+  "Return a string which is made of the concatenation of STRINGS."
+  (apply #'concatenate 'string strings))
+
+(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))
+
+(defun string-starts-with (prefix string &optional (compare #'string=))
+  (let ((prefix-length (length prefix)))
+    (and (>= (length string) prefix-length)
+         (funcall compare prefix string :end2 prefix-length))))
+
+;; QUEUE
+
+(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)))
+
+;; STREAMS
+
+(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)))
+  `(let ((,position (file-position ,stream)))
+     (unwind-protect (progn ,@forms)
+       (file-position ,stream ,position))))
+
+(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
+        (let ((seq (make-array (file-length in) :element-type element-type)))
+          (read-sequence seq in)
+          seq)
+        default)))
+
+;; FILES
+
+(defun native-namestring (pathname)
+  #+sbcl (sb-ext:native-namestring pathname)
+  #-sbcl (let (#+cmu (lisp::*ignore-wildcards* t))
+           (namestring pathname)))
+
+(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
+       ;; TODO(sterni): ECL, CCL
+       (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))))
+
+;; 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)))
+
+;; LAZY
+
+(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))))
+
+;; FIXNUMS
+
+(defmacro f++ (x &optional (delta 1))
+  "Same as INCF but hopefully optimised for fixnums."
+  `(setf ,x (+ (the fixnum ,x) (the fixnum ,delta))))
+
+;; TIME
+
+(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))
+
+(defvar +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)))
diff --git a/third_party/lisp/mime4cl/mime.lisp b/third_party/lisp/mime4cl/mime.lisp
new file mode 100644
index 000000000000..3cdac4b26b6f
--- /dev/null
+++ b/third_party/lisp/mime4cl/mime.lisp
@@ -0,0 +1,1049 @@
+;;;  mime4cl.lisp --- MIME primitives for Common Lisp
+
+;;;  Copyright (C) 2005-2008, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2021-2023 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)
+  (make-input-adapter (mime-body mime-part)))
+
+(defun mime-body-length (mime-part)
+  (let ((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) &body forms)
+  `(with-open-stream (,stream (mime-body-stream ,part))
+     ,@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."
+  ;; TODO(sterni): when-let
+  (let ((equal-position (position #\= string)))
+    (when equal-position
+      (let ((key (subseq string  0 equal-position)))
+        (if (= equal-position (1- (length string)))
+            (cons key "")
+            (let ((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))
+                        (let ((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\"))."
+  (let ((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."
+  (let ((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)))))
+
+(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)
+  (let ((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 = (let ((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 flexi-stream))
+  (let ((base (flexi-stream-root-stream stream)))
+    (if *lazy-mime-decode*
+        (setf (mime-body part)
+              (make-file-portion :data (etypecase base
+                                         (vector-stream
+                                          (flexi-streams::vector-stream-vector base))
+                                         (file-stream
+                                          (pathname base)))
+                                 :encoding (mime-encoding part)
+                                 :start (flexi-stream-position stream)
+                                 :end (flexi-stream-bound 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 vector-stream))
+  (if *lazy-mime-decode*
+      (setf (mime-body part)
+            (make-file-portion :data (flexi-streams::vector-stream-vector stream)
+                               :encoding (mime-encoding part)
+                               :start (flexi-streams::vector-stream-index 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)
+    (let ((offsets (index-multipart-parts stream (get-mime-type-parameter part :boundary))))
+      (setf (mime-parts part)
+            (mapcar #'(lambda (p)
+                        (destructuring-bind (start . end) p
+                          (let ((*default-type* (if (eq :digest (mime-subtype part))
+                                                    '("message" "rfc822" ())
+                                                    '("text" "plain" (("charset" . "us-ascii")))))
+                                (in (make-positioned-flexi-input-stream stream
+                                                                        :position start
+                                                                        :bound end
+                                                                        :ignore-close t)))
+                            (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)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defvar +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)
+  (let ((elt (assoc name headers :test #'string-equal)))
+    (values (cdr elt) (car elt))))
+
+(defun (setf header) (value name headers)
+  (let ((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."
+  (let ((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."
+  (let ((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))
+  (mime-message (flexi-streams:string-to-octets msg)))
+
+(defmethod mime-message ((msg vector))
+  (with-input-from-sequence (in msg)
+    (mime-message in)))
+
+(defmethod mime-message ((msg pathname))
+  (with-open-file (in msg :element-type '(unsigned-byte 8))
+    (mime-message in)))
+
+(defmethod mime-message ((msg flexi-stream))
+  (read-mime-message msg))
+
+(defmethod mime-message ((msg stream))
+  (read-mime-message (make-flexi-stream msg)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(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))
+  (let ((body (mime-body part)))
+    (make-instance (case (mime-encoding part)
+                     (:base64
+                      'base64-encoder-input-stream)
+                     (:quoted-printable
+                      'quoted-printable-encoder-input-stream)
+                     (otherwise
+                      '8bit-encoder-input-stream))
+                   :underlying-stream
+                   (make-input-adapter 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))
+  (let ((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
+     (let ((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))
+  (let ((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
+      (let ((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))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgeneric 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 000000000000..f3b429eafbf7
--- /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 000000000000..6528f115d47a
--- /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 000000000000..94b9e6b39053
--- /dev/null
+++ b/third_party/lisp/mime4cl/package.lisp
@@ -0,0 +1,103 @@
+;;;  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 :mime4cl-ex-sclf :trivial-gray-streams :flexi-streams)
+  (: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
+           #:encode-base64-stream
+           #:encode-base64-sequence
+           #:parse-RFC2047-text
+           #:decode-RFC2047
+           #: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
+           #: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
+           ;; streams.lisp
+           #:redirect-stream
+           ))
diff --git a/third_party/lisp/mime4cl/streams.lisp b/third_party/lisp/mime4cl/streams.lisp
new file mode 100644
index 000000000000..71a32d84e461
--- /dev/null
+++ b/third_party/lisp/mime4cl/streams.lisp
@@ -0,0 +1,274 @@
+;;; streams.lisp --- En/De-coding Streams
+
+;;; Copyright (C) 2012 by Walter C. Pelissero
+;;; Copyright (C) 2021-2023 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)
+
+(defun flexi-stream-root-stream (stream)
+  "Return the non FLEXI-STREAM stream a given chain of FLEXI-STREAMs is based on."
+  (if (typep stream 'flexi-stream)
+      (flexi-stream-root-stream (flexi-stream-stream stream))
+      stream))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defclass coder-stream-mixin ()
+  ((real-stream :type stream
+                :initarg :underlying-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)
+  ())
+
+;; TODO(sterni): temporary, ugly measure to make flexi-streams happy
+(defmethod stream-element-type ((stream coder-input-stream-mixin))
+  (declare (ignore stream))
+  '(unsigned-byte 8))
+
+(defclass quoted-printable-decoder-stream (coder-input-stream-mixin quoted-printable-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 :UNDERLYING-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 (let ((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)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun make-custom-flexi-stream (class stream other-args)
+  (apply #'make-instance
+         class
+         :stream stream
+         (mapcar (lambda (x)
+                   ;; make-flexi-stream has a discrepancy between :initarg of
+                   ;; make-instance and its &key which we mirror here.
+                   (if (eq x :external-format) :flexi-stream-external-format x))
+                 other-args)))
+
+(defclass adapter-flexi-input-stream (flexi-input-stream)
+  ((ignore-close
+    :initform nil
+    :initarg :ignore-close
+    :documentation
+    "If T, calling CLOSE on the stream does nothing.
+If NIL, the underlying stream is closed."))
+  (:documentation "FLEXI-STREAM that does not close the underlying stream on
+CLOSE if :IGNORE-CLOSE is T."))
+
+(defmethod close ((stream adapter-flexi-input-stream) &key abort)
+  (declare (ignore abort))
+  (with-slots (ignore-close) stream
+    (unless ignore-close
+      (call-next-method))))
+
+(defun make-input-adapter (source)
+  (etypecase source
+    ;; If it's already a stream, we need to make sure it's not closed by the adapter
+    (stream
+     (assert (input-stream-p source))
+     (if (and (typep source 'adapter-flexi-input-stream)
+              (slot-value source 'ignore-close))
+         source ; already ignores CLOSE
+         (make-adapter-flexi-input-stream source :ignore-close t)))
+    ;; TODO(sterni): is this necessary? (maybe with (not *lazy-mime-decode*)?)
+    (string
+     (make-input-adapter (string-to-octets source)))
+    ((vector (unsigned-byte 8))
+     (make-in-memory-input-stream source))
+    (pathname
+     (make-flexi-stream (open source :element-type '(unsigned-byte 8))))
+    (file-portion
+     (open-decoded-file-portion source))))
+
+(defun make-adapter-flexi-input-stream (stream &rest args)
+  "Create a ADAPTER-FLEXI-INPUT-STREAM. Accepts the same keyword arguments as
+MAKE-FLEXI-STREAM as well as :IGNORE-CLOSE. If T, the underlying stream is not
+closed."
+  (make-custom-flexi-stream 'adapter-flexi-input-stream stream args))
+
+(defclass positioned-flexi-input-stream (adapter-flexi-input-stream)
+  ()
+  (:documentation
+   "FLEXI-INPUT-STREAM that automatically advances the underlying :STREAM to
+the location given by :POSITION. This uses FILE-POSITION internally, so it'll
+only works if the underlying stream position is tracked in bytes. Note that
+the underlying stream is still advanced, so having multiple instances of
+POSITIONED-FLEXI-INPUT-STREAM based with the same underlying stream won't work
+reliably.
+Also supports :IGNORE-CLOSE of ADAPTER-FLEXI-INPUT-STREAM."))
+
+(defmethod initialize-instance ((stream positioned-flexi-input-stream)
+                                &key &allow-other-keys)
+  (call-next-method)
+  ;; The :POSITION initarg is only informational for flexi-streams: It assumes
+  ;; it is were the stream it got is already at and continuously updates it
+  ;; for querying (via FLEXI-STREAM-POSITION) and bound checking.
+  ;; Since we have streams that are not positioned correctly, we need to do this
+  ;; here using FILE-POSITION. Note that assumes the underlying implementation
+  ;; uses bytes for FILE-POSITION which is not guaranteed (probably some streams
+  ;; even in SBCL don't).
+  (file-position (flexi-stream-stream stream) (flexi-stream-position stream)))
+
+(defun make-positioned-flexi-input-stream (stream &rest args)
+  "Create a POSITIONED-FLEXI-INPUT-STREAM. Accepts the same keyword arguments as
+MAKE-FLEXI-STREAM as well as :IGNORE-CLOSE. Causes the FILE-POSITION of STREAM to
+be modified to match the :POSITION argument."
+  (make-custom-flexi-stream 'positioned-flexi-input-stream stream args))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; TODO(sterni): test correct behavior with END NIL
+(defstruct file-portion
+  data                                  ; string or a pathname
+  encoding
+  start
+  end)
+
+(defun open-decoded-file-portion (file-portion)
+  (with-slots (data encoding start end)
+      file-portion
+    (let* ((binary-stream
+             (etypecase data
+               (pathname
+                (open data :element-type '(unsigned-byte 8)))
+               ((vector (unsigned-byte 8))
+                (flexi-streams:make-in-memory-input-stream data))
+               (stream
+                ;; TODO(sterni): assert that bytes/flexi-stream
+                data)))
+           (params (ccase encoding
+                     ((:quoted-printable :base64) '(:external-format :us-ascii))
+                     (:8bit '(:element-type (unsigned-byte 8)))
+                     (:7bit '(:external-format :us-ascii))))
+           (portion-stream (apply #'make-positioned-flexi-input-stream
+                                  binary-stream
+                                  :position start
+                                  :bound end
+                                  ;; if data is a stream we can't have a
+                                  ;; FILE-PORTION without modifying it when
+                                  ;; reading etc. The least we can do, though,
+                                  ;; is forgo destroying it.
+                                  :ignore-close (typep data 'stream)
+                                  params))
+           (needs-decoder-stream (member encoding '(:quoted-printable
+                                                    :base64))))
+
+      (if needs-decoder-stream
+          (make-instance
+           (ccase encoding
+             (:quoted-printable 'quoted-printable-decoder-stream)
+             (:base64 'qbase64:decode-stream))
+           :underlying-stream portion-stream)
+          portion-stream))))
diff --git a/third_party/lisp/mime4cl/test/address.lisp b/third_party/lisp/mime4cl/test/address.lisp
new file mode 100644
index 000000000000..a3653985c40e
--- /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 000000000000..6b22b3f6a287
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/endec.lisp
@@ -0,0 +1,184 @@
+;;;  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
+         (qbase64:decode-string "U29tZSByYW5kb20gc3RyaW5nLg=="))
+  "Some random string.")
+
+(deftest base64.4
+    (map 'string #'code-char
+         (qbase64:decode-string "U29tZSByYW5kb20gc3RyaW5nLg=="))
+  "Some random string.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(deftest RFC2047.1
+    (parse-RFC2047-text "foo bar")
+  ("foo bar"))
+
+;; from RFC2047 section 8
+(deftest RFC2047.2
+    (decode-RFC2047 "=?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>")
+  "Keith Moore <moore@cs.utk.edu>")
+
+;; from RFC2047 section 8
+(deftest RFC2047.3
+    (decode-RFC2047 "=?ISO-8859-1?Q?Olle_J=E4rnefors?=")
+  "Olle Järnefors")
+
+;; from RFC2047 section 8
+(deftest RFC2047.4
+    (decode-RFC2047 "Nathaniel Borenstein <nsb@thumper.bellcore.com> (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)")
+  "Nathaniel Borenstein <nsb@thumper.bellcore.com> (םולש ןב ילטפנ)")
+
+;; from RFC2047 section 8
+(deftest RFC2047.5
+  (decode-RFC2047 "=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>")
+  "Keld Jørn Simonsen <keld@dkuug.dk>")
+
+(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 ((*tmp-file-defaults* (make-pathname :defaults #.(or *load-pathname* *compile-file-pathname*)
+                                                   :type "encoded-data")))
+      (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: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 000000000000..dbd1dd996dcc
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/mime.lisp
@@ -0,0 +1,41 @@
+;;; mime.lisp --- MIME regression tests
+
+;;; Copyright (C) 2012 by Walter C. Pelissero
+;;; Copyright (C) 2021-2023 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"")))
+
+(loop
+ for f in (directory (make-pathname :defaults *samples-directory*
+                                    :name :wild
+                                    :type "msg"))
+ for i from 1
+ do
+ (add-test (intern (format nil "MIME.~A" i))
+           `(let* ((orig (mime-message ,f))
+                   (dup (mime-message
+                         (with-output-to-string (out) (encode-mime-part orig out)))))
+              (mime= orig dup))
+           t))
diff --git a/third_party/lisp/mime4cl/test/package.lisp b/third_party/lisp/mime4cl/test/package.lisp
new file mode 100644
index 000000000000..965680448fe4
--- /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 :mime4cl-ex-sclf)
+  (:export))
diff --git a/third_party/lisp/mime4cl/test/rt.lisp b/third_party/lisp/mime4cl/test/rt.lisp
new file mode 100644
index 000000000000..3f3aa5c56cd3
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/rt.lisp
@@ -0,0 +1,258 @@
+#|----------------------------------------------------------------------------|
+ | Copyright 1990 by the Massachusetts Institute of Technology, Cambridge MA. |
+ | Copyright 2023 by the TVL Authors                                          |
+ |                                                                            |
+ | 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 #:add-test #: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-test (name form &rest values)
+  (funcall #'add-entry (append (list '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/samples/sample1.msg b/third_party/lisp/mime4cl/test/samples/sample1.msg
new file mode 100644
index 000000000000..662a9fab341e
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/samples/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/mime4cl/test/temp-file.lisp b/third_party/lisp/mime4cl/test/temp-file.lisp
new file mode 100644
index 000000000000..554f35844b46
--- /dev/null
+++ b/third_party/lisp/mime4cl/test/temp-file.lisp
@@ -0,0 +1,72 @@
+;;; temp-file.lisp --- temporary file creation
+
+;;;  Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2022 The TVL Authors
+
+;;;  Author: Walter C. Pelissero <walter@pelissero.de>
+;;;  Project: mime4cl
+;;;
+;;;  Code taken from SCLF
+
+#+cmu (ext:file-comment "$Module: temp-file.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 :mime4cl-tests)
+
+(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."
+  `(let ((,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))))))
diff --git a/third_party/lisp/moptilities.nix b/third_party/lisp/moptilities.nix
new file mode 100644
index 000000000000..d38fbcb9469a
--- /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 000000000000..b71f439c939a
--- /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 000000000000..82a8fe48bbfb
--- /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 000000000000..5051f60d6b86
--- /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 000000000000..223ede7de3ec
--- /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 000000000000..2e9580706346
--- /dev/null
+++ b/third_party/lisp/npg/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/third_party/lisp/npg/README b/third_party/lisp/npg/README
new file mode 100644
index 000000000000..a1661e744a37
--- /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 000000000000..af7ec53eaf93
--- /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 000000000000..a45ac614f716
--- /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 000000000000..9ebd45a169ce
--- /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 000000000000..1e35186d6c8c
--- /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 000000000000..8b64f5cc0a7b
--- /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 000000000000..783f071fc5d9
--- /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 000000000000..b405f7b5f19e
--- /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 000000000000..c15d26fe394e
--- /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 000000000000..e90824108ed1
--- /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 000000000000..61b0b1fddbda
--- /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 000000000000..23c67c2d9c30
--- /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 000000000000..d594ff1a1cf7
--- /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 000000000000..25e0625c20bc
--- /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 000000000000..af481499207f
--- /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 000000000000..f7146ba93f1f
--- /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/qbase64/coreutils-base64.patch b/third_party/lisp/qbase64/coreutils-base64.patch
new file mode 100644
index 000000000000..5a2f2a9f0864
--- /dev/null
+++ b/third_party/lisp/qbase64/coreutils-base64.patch
@@ -0,0 +1,13 @@
+diff --git a/qbase64-test.lisp b/qbase64-test.lisp
+index 310fdf3..b92abb5 100644
+--- a/qbase64-test.lisp
++++ b/qbase64-test.lisp
+@@ -14,7 +14,7 @@
+       (with-open-temporary-file (tmp :direction :output :element-type '(unsigned-byte 8))
+         (write-sequence bytes tmp)
+         (force-output tmp)
+-        (let* ((encoded (uiop:run-program `("base64" "-b" ,(format nil "~A" linebreak) "-i" ,(namestring tmp)) :output (if (zerop linebreak) '(:string :stripped t) :string)))
++        (let* ((encoded (uiop:run-program `("base64" "-w" ,(format nil "~A" linebreak) ,(namestring tmp)) :output (if (zerop linebreak) '(:string :stripped t) :string) :error-output *error-output*))
+                (length (length encoded)))
+           (cond ((and (> length 1)
+                       (string= (subseq encoded (- length 2))
diff --git a/third_party/lisp/qbase64/default.nix b/third_party/lisp/qbase64/default.nix
new file mode 100644
index 000000000000..40a93e04f062
--- /dev/null
+++ b/third_party/lisp/qbase64/default.nix
@@ -0,0 +1,57 @@
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.applyPatches {
+    src = pkgs.fetchFromGitHub {
+      owner = "chaitanyagupta";
+      repo = "qbase64";
+      rev = "4ac193ed6b35a867ca453ed74acc128c9a077407";
+      sha256 = "06daqqfdd51wkx0pyxgz7zq4ibzsqsgn3qs04jabx67gyybgnmjm";
+    };
+
+    patches = [
+      # qbase64 expects macOS base64
+      ./coreutils-base64.patch
+    ];
+  };
+
+  getSrcs = builtins.map (p: "${src}/${p}");
+
+in
+
+depot.nix.buildLisp.library {
+  name = "qbase64";
+
+  srcs = getSrcs [
+    "package.lisp"
+    "utils.lisp"
+    "stream-utils.lisp"
+    "qbase64.lisp"
+  ];
+
+  deps = [
+    depot.third_party.lisp.trivial-gray-streams
+    depot.third_party.lisp.metabang-bind
+  ];
+
+  tests = {
+    name = "qbase64-tests";
+
+    srcs = getSrcs [
+      "qbase64-test.lisp"
+    ];
+
+    deps = [
+      {
+        sbcl = depot.nix.buildLisp.bundled "uiop";
+        default = depot.nix.buildLisp.bundled "asdf";
+      }
+      depot.third_party.lisp.fiveam
+      depot.third_party.lisp.cl-fad
+    ];
+
+    expression = ''
+      (fiveam:run! '(qbase64-test::encoder 'qbase64-test::decoder))
+    '';
+  };
+}
diff --git a/third_party/lisp/quasiquote_2/README.md b/third_party/lisp/quasiquote_2/README.md
new file mode 100644
index 000000000000..2d590a0564ae
--- /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 000000000000..521c384787fe
--- /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 000000000000..6ebeb47d081e
--- /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 000000000000..9b140ef84c32
--- /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 000000000000..3acfd32b80e6
--- /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 000000000000..10043fe0ecbc
--- /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 000000000000..7c4c5a30c98e
--- /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 000000000000..df6c43e21d77
--- /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 000000000000..6c8ab08cc1af
--- /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 000000000000..b82a490c9d71
--- /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 000000000000..fc7d4e306713
--- /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 000000000000..9c4da4a02b25
--- /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 000000000000..9e5838c3c545
--- /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 000000000000..486e1c1ac8d3
--- /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/split-sequence.nix b/third_party/lisp/split-sequence.nix
new file mode 100644
index 000000000000..4e8f723c3182
--- /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 000000000000..27949e8be1e4
--- /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 000000000000..02abac54a853
--- /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 000000000000..74224df60d91
--- /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 000000000000..62a30f1e94f3
--- /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 000000000000..70a6e19d48a9
--- /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 000000000000..c85fe2accbb9
--- /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 000000000000..b097a3d0ee67
--- /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 000000000000..f98c029d3688
--- /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 000000000000..248296113263
--- /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 000000000000..5d6d04535f0c
--- /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 000000000000..589a3a0cfc92
--- /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 000000000000..bf4c55fe55b3
--- /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/napalm/default.nix b/third_party/napalm/default.nix
new file mode 100644
index 000000000000..e85c360ba91e
--- /dev/null
+++ b/third_party/napalm/default.nix
@@ -0,0 +1,7 @@
+{ depot, pkgs, ... }:
+
+pkgs.callPackage depot.third_party.sources.napalm { } // {
+  meta.ci.targets = [
+    "napalm-registry"
+  ];
+}
diff --git a/third_party/nixpkgs/default.nix b/third_party/nixpkgs/default.nix
new file mode 100644
index 000000000000..2f765862bdb8
--- /dev/null
+++ b/third_party/nixpkgs/default.nix
@@ -0,0 +1,81 @@
+# 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
+, localSystem ? externalArgs.localSystem or builtins.currentSystem
+, ...
+}:
+
+let
+  # Arguments passed to both the stable nixpkgs and the main, unstable one.
+  # Includes everything but overlays which are only passed to unstable nixpkgs.
+  commonNixpkgsArgs = {
+    # 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;
+        # Forbids our meta.ci attribute
+        # https://github.com/NixOS/nixpkgs/pull/191171#issuecomment-1260650771
+        checkMeta = false;
+      };
+
+    inherit localSystem;
+  };
+
+  # 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 commonNixpkgsArgs;
+
+  # 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: {
+    inherit (stableNixpkgs)
+      # binaryen does not build on unstable as of 2022-08-22
+      binaryen
+
+      # mysql80 is broken as of 2023-04-26, but should work after the next
+      # staging-next cycle: https://github.com/NixOS/nixpkgs/issues/226673
+      mysql80
+      ;
+  };
+
+  # 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 (commonNixpkgsArgs // {
+  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
+    (import depot.third_party.sources.rust-overlay)
+  ] else [ ]);
+})
diff --git a/third_party/nsfv/default.nix b/third_party/nsfv/default.nix
new file mode 100644
index 000000000000..26d5488c42ca
--- /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 000000000000..a64022791462
--- /dev/null
+++ b/third_party/overlays/dhall/OWNERS
@@ -0,0 +1 @@
+Profpatsch
diff --git a/third_party/overlays/dhall/default.nix b/third_party/overlays/dhall/default.nix
new file mode 100644
index 000000000000..462503599966
--- /dev/null
+++ b/third_party/overlays/dhall/default.nix
@@ -0,0 +1,30 @@
+{ ... }:
+
+self: super:
+
+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/dce9acbb99776a7f1344db4751d6080380f76f57.tar.gz";
+        sha256 = "0ckp6515gfvbxm08yyll87d9vg8sq2l21gwav2npzvwc3xz2lccf";
+      })
+      { pkgs = self; };
+in
+{
+  # ATTN: see the haskell overlay for some overrides we need.
+
+  # dhall = easy-dhall-nix.dhall-simple;
+  # dhall-nix = easy-dhall-nix.dhall-nix-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;
+  # 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 000000000000..66579c33abce
--- /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 000000000000..341feb501507
--- /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 000000000000..2a528eaa8a3a
--- /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 000000000000..8c4871fb084a
--- /dev/null
+++ b/third_party/overlays/haskell/default.nix
@@ -0,0 +1,48 @@
+# 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
+  haskellLib = self.haskell.lib.compose;
+  dhall-source = subdir: pkg:
+    haskellLib.overrideSrc
+      {
+        src = "${super.fetchFromGitHub {
+          owner = "Profpatsch";
+          repo = "dhall-haskell";
+          # https://github.com/dhall-lang/dhall-haskell/pull/2426
+          rev = "5e3a407d8ac826597d935d8398825a0ca73fc4e9";
+          sha256 = "005plj6kgxlkm9npaq07kmsgmiqk50dpwb9li9w1ly4aj1zgfjnd";
+        }}/${subdir}";
+      }
+      (haskellLib.overrideCabal { patches = [ ]; } pkg);
+
+
+in
+{
+  haskellPackages = super.haskellPackages.override {
+    overrides = hsSelf: hsSuper: {
+      # TODO: this is to fix a bug in dhall-nix
+      dhall = dhall-source "dhall" hsSuper.dhall;
+      dhall-nix = dhall-source "dhall-nix" hsSuper.dhall-nix;
+    };
+  };
+
+  haskell = lib.recursiveUpdate super.haskell {
+    packages.ghc8107 = super.haskell.packages.ghc8107.override {
+      overrides = hsSelf: hsSuper: {
+        # 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 { };
+
+        # TODO(grfn): port to brick 1.4 (EventM gains an additional type argument in 1.0)
+        brick = hsSelf.callPackage ./extra-pkgs/brick-0.73.nix { };
+      };
+    };
+  };
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/brick-0.73.nix b/third_party/overlays/haskell/extra-pkgs/brick-0.73.nix
new file mode 100644
index 000000000000..c5e2883c751f
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/brick-0.73.nix
@@ -0,0 +1,70 @@
+{ mkDerivation
+, base
+, bytestring
+, config-ini
+, containers
+, contravariant
+, data-clist
+, deepseq
+, directory
+, dlist
+, exceptions
+, filepath
+, lib
+, microlens
+, microlens-mtl
+, microlens-th
+, QuickCheck
+, stm
+, template-haskell
+, text
+, text-zipper
+, transformers
+, unix
+, vector
+, vty
+, word-wrap
+}:
+mkDerivation {
+  pname = "brick";
+  version = "0.73";
+  sha256 = "741c8d0717f0ab5addd5d3acc88cb36d645a0c73907bde509b2fd9d9bc02039c";
+  isLibrary = true;
+  isExecutable = true;
+  libraryHaskellDepends = [
+    base
+    bytestring
+    config-ini
+    containers
+    contravariant
+    data-clist
+    deepseq
+    directory
+    dlist
+    exceptions
+    filepath
+    microlens
+    microlens-mtl
+    microlens-th
+    stm
+    template-haskell
+    text
+    text-zipper
+    transformers
+    unix
+    vector
+    vty
+    word-wrap
+  ];
+  testHaskellDepends = [
+    base
+    containers
+    microlens
+    QuickCheck
+    vector
+    vty
+  ];
+  homepage = "https://github.com/jtdaugherty/brick/";
+  description = "A declarative terminal user interface library";
+  license = lib.licenses.bsd3;
+}
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 000000000000..1626eca7be4b
--- /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 000000000000..c00f5a1a8d9b
--- /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/buf-tests-dont-use-file-transport.patch b/third_party/overlays/patches/buf-tests-dont-use-file-transport.patch
new file mode 100644
index 000000000000..34be80eb361d
--- /dev/null
+++ b/third_party/overlays/patches/buf-tests-dont-use-file-transport.patch
@@ -0,0 +1,64 @@
+commit e9219b88de5ed37af337ee2d2e71e7ec7c0aad1b
+Author: Robbert van Ginkel <rvanginkel@buf.build>
+Date:   Thu Oct 20 16:43:28 2022 -0400
+
+    Fix git unit test by using fake git server rather than file:// (#1518)
+    
+    More recent versions of git fix a CVE by disabling some usage of the
+    `file://` transport, see
+    https://github.blog/2022-10-18-git-security-vulnerabilities-announced/#cve-2022-39253.
+    We were using this transport in tests.
+    
+    Instead, use https://git-scm.com/docs/git-http-backend to serve up this
+    repository locally so we don't have to use the file protocol. This
+    should be a more accurate tests, since we mostly expect submodules to
+    come from servers.
+
+diff --git a/.golangci.yml b/.golangci.yml
+index 318d1171..865e03e7 100644
+--- a/.golangci.yml
++++ b/.golangci.yml
+@@ -136,3 +136,8 @@ issues:
+     - linters:
+         - containedctx
+       path: private/bufpkg/bufmodule/bufmoduleprotocompile
++      # We should be able to use net/http/cgi in a unit test, in addition the CVE mentions only versions of go < 1.6.3 are affected.
++    - linters:
++        - gosec
++      path: private/pkg/git/git_test.go
++      text: "G504:"
+diff --git a/private/pkg/git/git_test.go b/private/pkg/git/git_test.go
+index 7b77b6cd..7132054e 100644
+--- a/private/pkg/git/git_test.go
++++ b/private/pkg/git/git_test.go
+@@ -17,6 +17,8 @@ package git
+ import (
+ 	"context"
+ 	"errors"
++	"net/http/cgi"
++	"net/http/httptest"
+ 	"os"
+ 	"os/exec"
+ 	"path/filepath"
+@@ -213,6 +215,21 @@ func createGitDirs(
+ 	runCommand(ctx, t, container, runner, "git", "-C", submodulePath, "add", "test.proto")
+ 	runCommand(ctx, t, container, runner, "git", "-C", submodulePath, "commit", "-m", "commit 0")
+ 
++	gitExecPath, err := command.RunStdout(ctx, container, runner, "git", "--exec-path")
++	require.NoError(t, err)
++	t.Log(filepath.Join(string(gitExecPath), "git-http-backend"))
++	// https://git-scm.com/docs/git-http-backend#_description
++	f, err := os.Create(filepath.Join(submodulePath, ".git", "git-daemon-export-ok"))
++	require.NoError(t, err)
++	require.NoError(t, f.Close())
++	server := httptest.NewServer(&cgi.Handler{
++		Path: filepath.Join(strings.TrimSpace(string(gitExecPath)), "git-http-backend"),
++		Dir:  submodulePath,
++		Env:  []string{"GIT_PROJECT_ROOT=" + submodulePath},
++	})
++	t.Cleanup(server.Close)
++	submodulePath = server.URL
++
+ 	originPath := filepath.Join(tmpDir, "origin")
+ 	require.NoError(t, os.MkdirAll(originPath, 0777))
+ 	runCommand(ctx, t, container, runner, "git", "-C", originPath, "init")
diff --git a/third_party/overlays/patches/notmuch-dottime.patch b/third_party/overlays/patches/notmuch-dottime.patch
new file mode 100644
index 000000000000..7a9cfc6cc2a9
--- /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 000000000000..3163a95db301
--- /dev/null
+++ b/third_party/overlays/tvl.nix
@@ -0,0 +1,128 @@
+# This overlay is used to make TVL-specific modifications in the
+# nixpkgs tree, where required.
+{ depot, localSystem, ... }:
+
+self: super:
+let
+  # Rollback Nix to a stable version (2.3) with backports for
+  # build-user problems applied.
+  nixSrc =
+    let
+      # branch 2.3-backport-await-users
+      rev = "46a91f2abaee85135cbb447c13957d3c5e9091a9";
+    in
+    self.fetchFromGitHub
+      {
+        owner = "tvlfyi";
+        repo = "nix";
+        inherit rev;
+        hash = "sha256:0rwyrh471c5y64axyd8vzzzmzlscg97fsrjbgbm1a93wnzxcvnvk";
+      } // { revCount = 0; shortRev = builtins.substring 0 7 rev; };
+
+  nixTarball = (scopedImport
+    {
+      # The tarball job always uses currentSystem which we need to purify
+      builtins = builtins // { currentSystem = localSystem; };
+    }
+    "${nixSrc}/release.nix"
+    {
+      nix = nixSrc;
+      nixpkgs = self.path;
+      systems = [ ];
+    }
+  ).tarball;
+in
+depot.nix.readTree.drvTargets {
+  nix_2_3 = super.nix_2_3.overrideAttrs (_: {
+    src = "${nixTarball}/tarballs/nix-${nixTarball.version}.tar.xz";
+  });
+  nix = self.nix_2_3;
+  nix_latest = super.nix;
+
+  # Too match telega in emacs-overlay or wherever
+  tdlib = super.tdlib.overrideAttrs (_: {
+    version = "1.8.12";
+    src = self.fetchFromGitHub {
+      owner = "tdlib";
+      repo = "td";
+      rev = "70bee089d492437ce931aa78446d89af3da182fc";
+      sha256 = "1m1mnvrk9nr3d3sq191i5y1bdgnp1hnq0c6iqybzmyswr501prz3";
+    };
+  });
+
+  home-manager = super.home-manager.overrideAttrs (_: {
+    src = depot.third_party.sources.home-manager;
+    version = "git-"
+      + builtins.substring 0 7 depot.third_party.sources.home-manager.rev;
+  });
+
+  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 = self.nix_2_3; };
+
+  # Avoid builds of mkShell derivations in CI.
+  mkShell = super.lib.makeOverridable (args: (super.mkShell args).overrideAttrs (_: {
+    passthru = {
+      meta.ci.skip = true;
+    };
+  }));
+
+  # Pin a newer version of crate2nix from git, which is not officially
+  # released but supports `dep:`
+  crate2nix = super.crate2nix.overrideAttrs (old: rec {
+    version = "0.11.0-rc.1";
+
+    src = self.fetchFromGitHub {
+      owner = "kolloch";
+      repo = "crate2nix";
+      rev = "v0.11.0-rc.1";
+      hash = "sha256:02yvn61w16sgkdxa019l5y9i2ybyk8h4516718gmarqxx5ws2kz8";
+    };
+
+    cargoDeps = old.cargoDeps.overrideAttrs (_: {
+      inherit src;
+      outputHash = "sha256:03yny9ikqzfpi2mr27r82g54an2s8k9lqi2i4fqalg7g0s2cr2yd";
+    });
+  });
+}
diff --git a/third_party/prometheus-fail2ban-exporter/default.nix b/third_party/prometheus-fail2ban-exporter/default.nix
new file mode 100644
index 000000000000..42ba0a14db4c
--- /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/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch b/third_party/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch
new file mode 100644
index 000000000000..bff2d4c200e2
--- /dev/null
+++ b/third_party/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch
@@ -0,0 +1,30 @@
+From 1719e904acf19499209b16a8a008f55390a7b5e2 Mon Sep 17 00:00:00 2001
+From: Vincent Ambo <mail@tazj.in>
+Date: Sun, 29 Jan 2023 13:36:12 +0300
+Subject: [PATCH] feat: always set the List-ID header even in -watch
+
+Without bothering to figure out exactly how this code path is usually
+triggered, always set a list ID when ingesting new emails in
+public-inbox-watch.
+---
+ lib/PublicInbox/Watch.pm | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/lib/PublicInbox/Watch.pm b/lib/PublicInbox/Watch.pm
+index 3f6fe21..147971c 100644
+--- a/lib/PublicInbox/Watch.pm
++++ b/lib/PublicInbox/Watch.pm
+@@ -188,6 +188,10 @@ sub _remove_spam {
+ sub import_eml ($$$) {
+ 	my ($self, $ibx, $eml) = @_;
+ 
++        # TVL-specific: always set the list-id header, regardless of
++        # any of the other logic below.
++        PublicInbox::MDA->set_list_headers($eml, $ibx);
++
+ 	# any header match means it's eligible for the inbox:
+ 	if (my $watch_hdrs = $ibx->{-watchheaders}) {
+ 		my $ok;
+-- 
+2.39.0
+
diff --git a/third_party/public-inbox/default.nix b/third_party/public-inbox/default.nix
new file mode 100644
index 000000000000..1a4b196f94b5
--- /dev/null
+++ b/third_party/public-inbox/default.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.public-inbox.overrideAttrs (old: {
+  patches = (old.patches or [ ]) ++ [
+    ./0001-feat-always-set-the-List-ID-header-even-in-watch.patch
+  ];
+
+  doCheck = false; # too slow, and nixpkgs already runs them
+})
diff --git a/third_party/python/broadlink/.gitignore b/third_party/python/broadlink/.gitignore
new file mode 100644
index 000000000000..0d20b6487c61
--- /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 000000000000..d8c801656bdf
--- /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 000000000000..8faba2be75fc
--- /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 000000000000..0c70c4beaed3
--- /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 000000000000..7e229e3eb557
--- /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 000000000000..5045c5c1082f
--- /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 000000000000..1c6b80b1483e
--- /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 000000000000..b1dcf300811d
--- /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 000000000000..2e388d749963
--- /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 000000000000..09f445bfd823
--- /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 000000000000..778f495fb58a
--- /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 000000000000..c45380536033
--- /dev/null
+++ b/third_party/rust-crates/OWNERS
@@ -0,0 +1,3 @@
+sterni
+Profpatsch
+zseri
diff --git a/third_party/rust-crates/default.nix b/third_party/rust-crates/default.nix
new file mode 100644
index 000000000000..e99de247bedc
--- /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.22";
+    edition = "2018";
+    dependencies = [ num-traits num-integer ];
+    features = [ "alloc" "std" ];
+    sha256 = "01vbn93ba1q2afq10qis41j847damk5ifgn1all337mcscl345fn";
+  };
+
+  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 000000000000..e0ea2b080a66
--- /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 000000000000..1a68245e9243
--- /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";
+  vendorHash = "sha256: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 000000000000..5894c9207905
--- /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 000000000000..575832c5dd14
--- /dev/null
+++ b/third_party/sources/sources.json
@@ -0,0 +1,122 @@
+{
+    "agenix": {
+        "branch": "main",
+        "description": "age-encrypted secrets for NixOS",
+        "homepage": "https://matrix.to/#/#agenix:nixos.org",
+        "owner": "ryantm",
+        "repo": "agenix",
+        "rev": "db5637d10f797bb251b94ef9040b237f4702cde3",
+        "sha256": "07f60gvrc2ianxa3s4lzjmlpps1vbi8scjiyz45813dbgavxcmix",
+        "type": "tarball",
+        "url": "https://github.com/ryantm/agenix/archive/db5637d10f797bb251b94ef9040b237f4702cde3.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": "77269bb9a0c99fdc5f356eac10b41172775194fc",
+        "sha256": "1m7qzrg7cgsf7l4caz71q1yjngyr48z9n8z701ppbdzk66ydfjfm",
+        "type": "tarball",
+        "url": "https://github.com/nix-community/emacs-overlay/archive/77269bb9a0c99fdc5f356eac10b41172775194fc.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "home-manager": {
+        "branch": "master",
+        "description": "Manage a user environment using Nix  [maintainer=@rycee] ",
+        "homepage": "https://nix-community.github.io/home-manager/",
+        "owner": "nix-community",
+        "repo": "home-manager",
+        "rev": "2d963854ae2499193c0c72fd67435fee34d3e4fd",
+        "sha256": "1851n81s5z5gliq9ymcw1k1z46rhgb3q3q8b7qiim2y8phbmdbhy",
+        "type": "tarball",
+        "url": "https://github.com/nix-community/home-manager/archive/2d963854ae2499193c0c72fd67435fee34d3e4fd.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "impermanence": {
+        "branch": "master",
+        "description": "Modules to help you handle persistent state on systems with ephemeral root storage [maintainer=@talyz]",
+        "homepage": "",
+        "owner": "nix-community",
+        "repo": "impermanence",
+        "rev": "89253fb1518063556edd5e54509c30ac3089d5e6",
+        "sha256": "095q3c1kyj7lpnn1i53c0158jh02avsm6xmkvql045xppkxfnk1b",
+        "type": "tarball",
+        "url": "https://github.com/nix-community/impermanence/archive/89253fb1518063556edd5e54509c30ac3089d5e6.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": "88cd22380154a2c36799fe8098888f0f59861a15",
+        "sha256": "1wxi05lz4p2484w7iyw4k4fkxsqq8lkcsyjb87i34i40yz6fxwk6",
+        "type": "tarball",
+        "url": "https://github.com/nmattia/naersk/archive/88cd22380154a2c36799fe8098888f0f59861a15.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "napalm": {
+        "branch": "master",
+        "description": "Support for building npm packages in Nix and lightweight npm registry [maintainer @nmattia]",
+        "homepage": "",
+        "owner": "nix-community",
+        "repo": "napalm",
+        "rev": "7c25a05cef52dc405f4688422ce0046ca94aadcf",
+        "sha256": "0aff7rikmyy4c2h6981vcv2n05xvllh2dqkhqxgz80z2nfapm7wb",
+        "type": "tarball",
+        "url": "https://github.com/nix-community/napalm/archive/7c25a05cef52dc405f4688422ce0046ca94aadcf.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": "4e37b4e55b60fb7d43d2b62deb51032a489bcbe8",
+        "sha256": "11a6j8kkk54518lncic5wa88fa5rski3rq6m9s7qlfngkqsihr0q",
+        "type": "tarball",
+        "url": "https://github.com/NixOS/nixpkgs/archive/4e37b4e55b60fb7d43d2b62deb51032a489bcbe8.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": "eabc38219184cc3e04a974fe31857d8e0eac098d",
+        "sha256": "04ffwp2gzq0hhz7siskw6qh9ys8ragp7285vi1zh8xjksxn1msc5",
+        "type": "tarball",
+        "url": "https://github.com/NixOS/nixpkgs/archive/eabc38219184cc3e04a974fe31857d8e0eac098d.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "rust-overlay": {
+        "branch": "master",
+        "description": "Pure and reproducible nix overlay of binary distributed rust toolchains",
+        "homepage": "",
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "d6ac24aa7ff658552ce57913df4b0cb823cf15ab",
+        "sha256": "0f7a6p5137vfl03qngksf9j0lqgj1scjhj40ixbdsjiv0bjnw8cr",
+        "type": "tarball",
+        "url": "https://github.com/oxalica/rust-overlay/archive/d6ac24aa7ff658552ce57913df4b0cb823cf15ab.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": "e162556b9e8c1542c187290453cbd322e8905f0c",
+        "sha256": "18s1zn7wyqw902lyi6r9k726mb6pj475gw4n4ng2danwsmfdnm9i",
+        "type": "tarball",
+        "url": "https://github.com/RustSec/advisory-db/archive/e162556b9e8c1542c187290453cbd322e8905f0c.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 000000000000..f798244d100f
--- /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}";
+  hash = "sha256:1hlqa4f9d44hq614ff8ivg8a6fwg48jwz11zsrlghjzky82cfraq";
+
+  vendorHash = "sha256: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/checks/default.nix b/tools/checks/default.nix
new file mode 100644
index 000000000000..89e4b809bc3f
--- /dev/null
+++ b/tools/checks/default.nix
@@ -0,0 +1,38 @@
+# Utilities for CI checks that work with the readTree-based CI.
+{ pkgs, ... }:
+
+let
+  inherit (pkgs.lib.strings) sanitizeDerivationName;
+in
+{
+  # Utility for verifying Terraform configuration.
+  #
+  # Expects to be passed a pre-configured Terraform derivation and a
+  # source path, and will do a dummy-initialisation and config
+  # validation inside of that Terraform configuration.
+  validateTerraform =
+    {
+      # Environment name to use (inconsequential, only for drv name)
+      name ? "main"
+    , # Terraform package to use. Should be pre-configured with the
+      # correct providers.
+      terraform ? pkgs.terraform
+    , # Source path for Terraform configuration. Be careful about
+      # relative imports. Use the 'subDir' parameter to optionally cd
+      # into a subdirectory of source, e.g. if there is a flat structure
+      # with modules.
+      src
+    , # Sub-directory of $src from which to run the check. Useful in
+      # case of relative Terraform imports from a code tree
+      subDir ? "."
+    , # Environment variables to pass to Terraform. Necessary in case of
+      # dummy environment variables that need to be set.
+      env ? { }
+    }:
+    pkgs.runCommand "tf-validate-${sanitizeDerivationName name}" env ''
+      cp -r ${src}/* . && chmod -R u+w .
+      cd ${subDir}
+      ${terraform}/bin/terraform init -upgrade -backend=false -input=false
+      ${terraform}/bin/terraform validate | tee $out
+    '';
+}
diff --git a/tools/cheddar/.gitignore b/tools/cheddar/.gitignore
new file mode 100644
index 000000000000..2f7896d1d136
--- /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 000000000000..e69de29bb2d1
--- /dev/null
+++ b/tools/cheddar/.skip-subtree
diff --git a/tools/cheddar/Cargo.lock b/tools/cheddar/Cargo.lock
new file mode 100644
index 000000000000..2787d79cb782
--- /dev/null
+++ b/tools/cheddar/Cargo.lock
@@ -0,0 +1,1587 @@
+# 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.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[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.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+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 = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+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 = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
+[[package]]
+name = "cc"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
+
+[[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 2.34.0",
+ "comrak",
+ "lazy_static",
+ "regex",
+ "rouille",
+ "serde",
+ "serde_json",
+ "syntect",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "chunked_transfer"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a"
+
+[[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 0.8.0",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "clap"
+version = "4.0.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39"
+dependencies = [
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "is-terminal",
+ "once_cell",
+ "strsim 0.10.0",
+ "termcolor",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "comrak"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c11e55664fcff7f4d37cc2adf3a1996913692f037312f4ab0909047fdd2bf962"
+dependencies = [
+ "clap 4.0.32",
+ "entities",
+ "memchr",
+ "once_cell",
+ "pest",
+ "pest_derive",
+ "regex",
+ "shell-words",
+ "syntect",
+ "typed-arena",
+ "unicode_categories",
+ "xdg",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+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 = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5add3fc1717409d029b20c5b6903fc0c0b02fa6741d820054f4a2efa5e5816fd"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4c87959ba14bc6fbc61df77c3fcfe180fc32b93538c4f1031dd802ccb5f2ff0"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69a3e162fde4e594ed2b07d0f83c6c67b745e7f28ce58c6df5e6b6bef99dfb59"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e7e2adeb6a0d4a282e581096b06e1791532b7d576dcde5ccd9382acf55db8e6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "deflate"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f"
+dependencies = [
+ "adler32",
+ "gzip-header",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[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 = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fancy-regex"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf"
+dependencies = [
+ "bit-set",
+ "regex",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+dependencies = [
+ "crc32fast",
+ "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.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gzip-header"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2"
+dependencies = [
+ "crc32fast",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[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 = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+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 = "io-lifetimes"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+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.139"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
+
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
+]
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[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.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+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",
+]
+
+[[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.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "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 = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "onig"
+version = "6.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
+dependencies = [
+ "bitflags",
+ "libc",
+ "once_cell",
+ "onig_sys",
+]
+
+[[package]]
+name = "onig_sys"
+version = "69.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "pest"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha1",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[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.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5"
+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.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+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.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[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.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f86e4c51a773f953f02bbab5fd049f004bfd384341d62da2a079aff812ab176"
+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 = "rustix"
+version = "0.36.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde"
+
+[[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 = "scratch"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
+
+[[package]]
+name = "serde"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[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 = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syntect"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8"
+dependencies = [
+ "bincode",
+ "bitflags",
+ "fancy-regex",
+ "flate2",
+ "fnv",
+ "lazy_static",
+ "once_cell",
+ "onig",
+ "plist",
+ "regex-syntax",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "thiserror",
+ "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 = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb20089a8ba2b69debd491f8d2d023761cbf196e999218c591fa1e7e15a21907"
+dependencies = [
+ "rustix",
+ "windows-sys",
+]
+
+[[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.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "time-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2"
+dependencies = [
+ "time-core",
+]
+
+[[package]]
+name = "tiny_http"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
+dependencies = [
+ "ascii",
+ "chunked_transfer",
+ "httpdate",
+ "log",
+]
+
+[[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 = "typed-arena"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae"
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[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.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "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.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[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.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+
+[[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 000000000000..d911b7c44682
--- /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.15"
+lazy_static = "1.4"
+rouille = "3.6"
+syntect = "5.0"
+serde_json = "1.0"
+regex = "1.7"
+
+[dependencies.serde]
+version = "1.0"
+features = [ "derive" ]
diff --git a/tools/cheddar/README.md b/tools/cheddar/README.md
new file mode 100644
index 000000000000..706f3b62d552
--- /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 000000000000..f70818d80177
--- /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 000000000000..d29f75dc3c9f
--- /dev/null
+++ b/tools/cheddar/default.nix
@@ -0,0 +1,24 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  doDoc = false;
+  doCheck = true;
+
+  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 000000000000..48c504d53590
--- /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 000000000000..c56789eca914
--- /dev/null
+++ b/tools/cheddar/src/lib.rs
@@ -0,0 +1,347 @@
+//! 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_uncompressed_data;
+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_uncompressed_data(include_bytes!(env!("BAT_SYNTAXES")))
+            .expect("failed to deserialise SyntaxSet");
+
+    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(line, &SYNTAXES)
+                .expect("highlight_line failed");
+            append_highlighted_html_for_styled_line(&regions[..], IncludeBackground::No, &mut buf)
+                .expect("appending HTML failed");
+        }
+
+        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_line(&linebuf, &SYNTAXES)
+            .expect("highlight_line failed");
+
+        append_highlighted_html_for_styled_line(
+            &regions[..],
+            IncludeBackground::IfDifferent(bg),
+            &mut outbuf,
+        )
+        .expect("appending highlighted HTML failed");
+
+        // 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 000000000000..4d22a1cf05e0
--- /dev/null
+++ b/tools/cheddar/src/tests.rs
@@ -0,0 +1,110 @@
+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>");
+}
+
+#[test]
+fn syntax_set_loaded() {
+    assert!(SYNTAXES.syntaxes().len() > 0)
+}
diff --git a/tools/crfo-approve.nix b/tools/crfo-approve.nix
new file mode 100644
index 000000000000..d4cff9e1b238
--- /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 000000000000..60af9f1378c5
--- /dev/null
+++ b/tools/depot-deps.nix
@@ -0,0 +1,33 @@
+# 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";
+  fetch-depot-inbox.attr = "tools.fetch-depot-inbox";
+  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-buildkite = {
+    attr = "ops.buildkite.terraform";
+    cmd = "terraform";
+  };
+
+  tf-glesys = {
+    attr = "ops.glesys.terraform";
+    cmd = "terraform";
+  };
+
+  tf-keycloak = {
+    attr = "ops.keycloak.terraform";
+    cmd = "terraform";
+  };
+}
diff --git a/tools/depotfmt.nix b/tools/depotfmt.nix
new file mode 100644
index 000000000000..4530469d1cfe
--- /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
+  '';
+
+  # TODO: Upgrade to Go 1.19 and reformat tree
+  config = pkgs.writeText "depot-treefmt-config" ''
+    [formatter.go]
+    command = "${pkgs.go_1_18}/bin/gofmt"
+    options = [ "-w" ]
+    includes = ["*.go"]
+
+    [formatter.tf]
+    command = "${terraformat}"
+    includes = [ "*.tf" ]
+
+    [formatter.nix]
+    command = "${pkgs.nixpkgs-fmt}/bin/nixpkgs-fmt"
+    includes = [ "*.nix" ]
+    excludes = [
+      "tvix/eval/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 000000000000..990b53b763b0
--- /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 000000000000..ffd359e5ff83
--- /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 000000000000..e9c86d25eec8
--- /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 000000000000..b819e9c14d2c
--- /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 000000000000..2446f6488f32
--- /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 000000000000..b167cb964214
--- /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 000000000000..4ddc81f563d3
--- /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 000000000000..45c9222313b4
--- /dev/null
+++ b/tools/emacs-pkgs/notable/OWNERS
@@ -0,0 +1 @@
+tazjin
diff --git a/tools/emacs-pkgs/notable/default.nix b/tools/emacs-pkgs/notable/default.nix
new file mode 100644
index 000000000000..f57b1c66ae3f
--- /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 000000000000..4668dd333c99
--- /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 000000000000..45c9222313b4
--- /dev/null
+++ b/tools/emacs-pkgs/passively/OWNERS
@@ -0,0 +1 @@
+tazjin
diff --git a/tools/emacs-pkgs/passively/README.md b/tools/emacs-pkgs/passively/README.md
new file mode 100644
index 000000000000..a5ac0d5a40bf
--- /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@tvl.su`.
+
+## 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 000000000000..ec59cc85fd8f
--- /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 000000000000..0d871f26add6
--- /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 000000000000..e775de5cdbe8
--- /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 000000000000..0055f87fd67f
--- /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 000000000000..d2800bc9413a
--- /dev/null
+++ b/tools/emacs-pkgs/tvl/OWNERS
@@ -0,0 +1 @@
+grfn
diff --git a/tools/emacs-pkgs/tvl/default.nix b/tools/emacs-pkgs/tvl/default.nix
new file mode 100644
index 000000000000..5dcc184bb521
--- /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 000000000000..8db718a8359d
--- /dev/null
+++ b/tools/emacs-pkgs/tvl/tvl.el
@@ -0,0 +1,244 @@
+;;; 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))
+
+(transient-define-suffix magit-gerrit-push-private ()
+  "Push a private change to Gerrit."
+  (interactive)
+  (magit-push-refspecs tvl-gerrit-remote
+                       (tvl--gerrit-ref tvl-target-branch
+                                        '("private"
+                                          "publish-comments"))
+                       nil))
+
+(transient-append-suffix
+  #'magit-push ["r"]
+  (list "Q" "push private change to Gerrit" #'magit-gerrit-push-private))
+
+(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 only-deps)
+     "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."
+
+     ;; TODO(sterni): this function asumes that we are using SBCL
+     ;;               - for determining the resulting wrapper's location
+     ;;               - for creating the dep-only wrapper
+
+     (interactive (list (read-string "Attribute: ")
+                        (yes-or-no-p "Only include dependencies? ")))
+     (lexical-let* ((outbuf (get-buffer-create (format "*depot-out/%s*" attribute)))
+                    (errbuf (get-buffer-create (format "*depot-errors/%s*" attribute)))
+                    (attr-display (if only-deps attribute (format "dependencies of %s" attribute)))
+                    (expression (if only-deps
+                                    (format "let d = import <depot> {}; in d.nix.buildLisp.sbcl.lispWith d.%s.lispDeps"
+                                            attribute)
+                                    (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" attr-display)
+       (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" attr-display lisp-path)
+                                (sly lisp-path)))
+                             (_ (with-current-buffer errbuf
+                                  (error "Failed to build %s:\nTried building '%s':\n%s" attr-display expression (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 000000000000..933d73ea71ae
--- /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/fetch-depot-inbox.nix b/tools/fetch-depot-inbox.nix
new file mode 100644
index 000000000000..e14ddf20e0a6
--- /dev/null
+++ b/tools/fetch-depot-inbox.nix
@@ -0,0 +1,49 @@
+# Wrapper script that uses offlineimap to fetch the depot inbox from
+# inbox.tvl.su.
+#
+# Run with the desired output directory as the only argument.
+#
+# Alternatively, users can browse the inbox on https://inbox.tvl.su
+# and interact with public-inbox in any other supported way (IMAP,
+# NNTP, git, etc.).
+{ pkgs, depot, ... }:
+
+let
+  config = pkgs.writeText "offlineimaprc" ''
+    [general]
+    accounts = depot
+
+    [Account depot]
+    localrepository = Local
+    remoterepository = Remote
+
+    [Repository Local]
+    type = Maildir
+    # localfolders set by CLI
+
+    [Repository Remote]
+    type = IMAP
+    ssl = yes
+    sslcacertfile = /etc/ssl/certs/ca-bundle.crt
+    remotehost = inbox.tvl.su
+    remoteuser = anonymous
+    remotepass = anonymous
+  '';
+in
+pkgs.writeShellScriptBin "fetch-depot-inbox" ''
+  readonly MAILDIR=''${1}
+
+  if [ -z "''${MAILDIR}" ]; then
+    echo "[inbox] must specify target maildir as the first argument!" >&2
+    exit 1
+  fi
+
+  if [ ! -d "''${MAILDIR}" ]; then
+    echo "[inbox] specified maildir must exist and be a directory!" >&2
+    exit 1
+  fi
+
+  echo "[inbox] Synchronising TVL depot inbox into ''${MAILDIR}"
+  ${pkgs.offlineimap}/bin/offlineimap -c ${config} \
+    -k "Repository_Local:localfolders=''${MAILDIR}"
+''
diff --git a/tools/gerrit-cli.nix b/tools/gerrit-cli.nix
new file mode 100644
index 000000000000..1606155a8068
--- /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 000000000000..e4efd89ea597
--- /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 000000000000..6192436c17b9
--- /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 000000000000..fa0a5d89a172
--- /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 000000000000..f3a615f81890
--- /dev/null
+++ b/tools/magrathea/mg.scm
@@ -0,0 +1,366 @@
+;; 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>]
+       mg run [<target>] [-- <arguments>]
+
+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
+  repl  - start a nix repl in the repository root
+  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 (repl args)
+  (process-execute "nix" (append (list "repl" "--show-trace" (repository-root)) args)))
+
+(define (execute-run t #!optional cmd-args)
+  (fprintf (current-error-port) "[mg] building target ~A~%" t)
+  (let* ((expr (nix-expr-for t))
+         (out
+          (receive (pipe _ pid)
+              ;; TODO(sterni): temporary gc root
+              (process "nix-build" (list "-E" expr "--no-out-link"))
+            (let ((stdout (string-chomp
+                           (let ((s (read-string #f pipe)))
+                             (if (eq? s #!eof) "" s)))))
+              (receive (_ _ status)
+                  (process-wait pid)
+                (when (not (eq? status 0))
+                  (mg-error (format "Couldn't build target ~A" t))
+                  (exit status))
+                stdout)))))
+
+    (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))]
+         [("--" . rest) (execute-run (empty-target) rest)]
+         [(target . ("--" . rest)) (execute-run (guarantee-success (parse-target target)) rest)]
+         ;; TODO(sterni): flag for selecting binary name
+         [_ (mg-error "usage: mg run [<target>] [-- <arguments>] (hint: use \"--\" to separate the `mg run [<target>]` invocation from the arguments you're passing to the built executable)")]))
+
+(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))]
+         [("repl" . _) (repl (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 000000000000..578eea392301
--- /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 000000000000..4948dd56eb1d
--- /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 000000000000..d64569567334
--- /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 000000000000..a879d030b868
--- /dev/null
+++ b/tools/nixery/README.md
@@ -0,0 +1,156 @@
+<div align="center">
+  <img src="https://nixery.dev/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 000000000000..8763e4cb8566
--- /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 000000000000..7f0bd7fffdb9
--- /dev/null
+++ b/tools/nixery/builder/builder.go
@@ -0,0 +1,527 @@
+// 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"
+	"github.com/im7mortal/kmutex"
+	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
+	UploadMutex *kmutex.Kmutex
+}
+
+// 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 {
+		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.
+		lw := func(w io.Writer) (string, error) {
+			tarhash, err := packStorePaths(&l, w)
+			if err != nil {
+				return "", err
+			}
+
+			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")
+
+			return tarhash, err
+		}
+
+		entry, err := uploadHashLayer(ctx, s, lh, l.MergeRating, lw)
+		if err != nil {
+			return nil, err
+		}
+
+		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, 0, func(w io.Writer) (string, 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 "sha256:" + slkey, gz.Close()
+	})
+
+	if err != nil {
+		return nil, err
+	}
+
+	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) (string, 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, mrating uint64, lw layerWriter) (*manifest.Entry, error) {
+	s.UploadMutex.Lock(key)
+	defer s.UploadMutex.Unlock(key)
+
+	if entry, cached := layerFromCache(ctx, s, key); cached {
+		return entry, nil
+	}
+
+	path := "staging/" + key
+	var tarhash string
+	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)
+
+		var err error
+		tarhash, 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,
+		TarHash:     tarhash,
+		MergeRating: mrating,
+	}
+
+	cacheLayer(ctx, s, key, entry)
+
+	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) (string, error) {
+		r := bytes.NewReader(c.Config)
+		_, err := io.Copy(w, r)
+		return "", err
+	}
+
+	if _, err = uploadHashLayer(ctx, s, c.SHA256, 0, 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 000000000000..507f3eb15a83
--- /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 000000000000..9e4283c0e5bb
--- /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 000000000000..24aec6391c45
--- /dev/null
+++ b/tools/nixery/cmd/server/main.go
@@ -0,0 +1,283 @@
+// 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"
+	"github.com/im7mortal/kmutex"
+	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,
+		UploadMutex: kmutex.New(),
+	}
+
+	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 000000000000..73ff5c835646
--- /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 000000000000..c7508a4d3af0
--- /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 000000000000..1d069511ee35
--- /dev/null
+++ b/tools/nixery/default.nix
@@ -0,0 +1,131 @@
+# 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 lib;
+
+  # 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; };
+
+  # Include the Nixery website into the Nix store, unless its being
+  # overridden to something else. Nixery will serve this as its front
+  # page when visited from a browser.
+  nixery-web = ./web;
+
+  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
+    vendorHash = "sha256-io9NCeZmjCZPLmII3ajXIsBWbT40XiW8ncXOuUDabbo=";
+
+    ldflags = [
+      "-s"
+      "-w"
+      "-X"
+      "main.version=${nixery-commit-hash}"
+    ];
+
+    nativeBuildInputs = [ makeWrapper ];
+    postInstall = ''
+      wrapProgram $out/bin/server \
+        --set WEB_DIR "${nixery-web}" \
+        --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/go.mod b/tools/nixery/go.mod
new file mode 100644
index 000000000000..9e896ffb4046
--- /dev/null
+++ b/tools/nixery/go.mod
@@ -0,0 +1,14 @@
+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/im7mortal/kmutex v1.0.1 // indirect
+	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 000000000000..5b6054fb6030
--- /dev/null
+++ b/tools/nixery/go.sum
@@ -0,0 +1,708 @@
+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/im7mortal/kmutex v1.0.1 h1:zAACzjwD+OEknDqnLdvRa/BhzFM872EBwKijviGLc9Q=
+github.com/im7mortal/kmutex v1.0.1/go.mod h1:f71c/Ugk/+58OHRAgvgzPP3QEiWGUjK13fd8ozfKWdo=
+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 000000000000..131a0cdbed77
--- /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 000000000000..06adc701efd4
--- /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 000000000000..d61514d2f62d
--- /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 000000000000..8485a4d30e9c
--- /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 000000000000..4b16768e4e89
--- /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 000000000000..b83ac3ed1ad8
--- /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 000000000000..efd9ed3404ec
--- /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 000000000000..7f8ab5479d7e
--- /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 000000000000..28022fe42f02
--- /dev/null
+++ b/tools/nixery/prepare-image/prepare-image.nix
@@ -0,0 +1,198 @@
+# 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-unstable"
+, 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;
+
+  # Workaround for a workaround in nixpkgs: Unquoted language
+  # identifiers can not start with numbers in Nix, but some package
+  # names start with numbers (such as `1password`).
+  #
+  # In nixpkgs convention, these identifiers are prefixed with
+  # underscores (e.g. `_1password`), however this is not accepted by
+  # the Docker registry protocol.
+  #
+  # To make this work, we detect these kinds of packages and add the
+  # missing underscore.
+  needsUnderscore = pkg: (builtins.match "^[0-9].*" pkg) != null;
+  normalisedPackages = map (p: if needsUnderscore p then "_${p}" else p) (fromJSON packages);
+
+  # 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) normalisedPackages);
+    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 (builtins.unsafeDiscardStringContext (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 (builtins.unsafeDiscardStringContext (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 000000000000..9d06e96ba29c
--- /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 000000000000..b91094722c48
--- /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 000000000000..3df4420f0fe1
--- /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 000000000000..752c6bbd8275
--- /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 000000000000..5500d61640d0
--- /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/nixery/web/index.html b/tools/nixery/web/index.html
new file mode 100644
index 000000000000..354c4913b272
--- /dev/null
+++ b/tools/nixery/web/index.html
@@ -0,0 +1,166 @@
+<!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="https://static.tvl.fyi/latest/tvl.css" media="all">
+  <link rel="icon" type="image/webp" href="/favicon.webp">
+  <title>Nixery</title>
+</head>
+<body class="light">
+  <img src="./nixery-logo.png" alt="Nixery">
+  <hr>
+
+  <p>
+    Welcome to this instance of Nixery, an ad-hoc container image registry that provides
+    packages from the <a href="https://nixos.org/nix">Nix</a> package manager.
+  </p>
+
+  <p>
+    You can pull container images from this registry
+    at <code><span class="registry-hostname">nixery.dev</span></code> by appending any
+    packages that you need in the URL, separated by slashes.
+  </p>
+
+  <noscript>
+    <p class="cheddar-callout cheddar-tip">
+      <strong>NOTE:</strong> When pulling from a private Nixery instance,
+      replace <code>nixery.dev</code> in the above examples with your registry address.
+    </p>
+  </noscript>
+
+  <h2><a href="#demo" aria-hidden="true" class="anchor" id="demo"></a>Demo</h2>
+
+  <noscript>
+    <p>
+      The interactive demo needs Javascript to run, but you can just read the Usage
+      instructions below instead
+    </p>
+  </noscript>
+
+  <script src="https://asciinema.org/a/262583.js" id="asciicast-262583" async data-autoplay="true" data-loop="true"></script>
+
+  <h2><a href="#usage" aria-hidden="true" class="anchor" id="usage"></a>Usage</h2>
+
+  <p>
+    These usage examples assume that you use Docker, but should not be much different for
+    other OCI-compatible platforms.
+  </p>
+
+  <p>
+    Pull an image from this registry, separating each package you want included by a
+    slash:
+  </p>
+
+  <pre style="background-color:#f6f8fa;padding:16px;"><span style="color:#323232;">docker pull <span class="registry-hostname">nixery.dev</span>/shell/git/htop</span></pre>
+
+  <p>
+    This gives you an image with <code>git</code>, <code>htop</code> and an interactively
+    configured shell. You could run it like this:
+  </p>
+
+  <pre style="background-color:#f6f8fa;padding:16px;"><span style="color:#323232;">docker run -ti <span class="registry-hostname">nixery.dev</span>/shell/git/htop bash</span></pre>
+
+  <p>
+    Each path segment corresponds either to a key in the Nix package set, or a
+    meta-package that automatically expands to several other packages.
+  </p>
+
+  <p>
+    Meta-packages <strong>must</strong> be the first path component if they are used.
+    Currently there are only two meta-packages:
+  </p>
+
+  <ul>
+    <li>
+      <p>
+        <code>shell</code>, which provides a <code>bash</code>-shell with interactive
+        configuration and standard tools like <code>coreutils</code></p>
+    </li>
+    <li>
+      <p><code>arm64</code>, which provides ARM64 binaries</p>
+    </li>
+  </ul>
+
+  <h2><a href="#faq" aria-hidden="true" class="anchor" id="faq"></a>FAQ</h2>
+
+  <h3>
+    <a href="#how-does-this-work" aria-hidden="true" class="anchor" id="how-does-this-work"></a>
+    How does this work?
+  </h3>
+
+  <p>
+    The short version is that we use the Nix package manager and an optimised
+    <a href="https://tazj.in/blog/nixery-layers">layering strategy</a>.
+  </p>
+
+  <p>
+    Check out <a href="https://www.youtube.com/watch?v=pOI9H4oeXqA">the Nixery talk</a>
+    from NixCon 2019 for more information.
+  </p>
+
+  <h3>
+    <a href="#should-i-depend-on-nixerydev-in-production" aria-hidden="true" class="anchor" id="should-i-depend-on-nixerydev-in-production"></a>
+    Should I depend on <code>nixery.dev</code> in production?
+  </h3>
+
+  <p>
+    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 <code>nixery.dev</code> is run on a best-effort basis and we make no guarantees
+    about availability.
+  </p>
+
+  <h3>
+    <a href="#who-made-this" aria-hidden="true" class="anchor" id="who-made-this"></a>
+    Who made this?
+  </h3>
+
+  <p>
+    Nixery was written by <a href="https://tazj.in">tazjin</a>, originally at Google.
+    These days Nixery is maintained by <a href="https://tvl.su">TVL</a>.
+  </p>
+  <p>
+    Nixery would not be possible without the many people that have contributed to Nix and
+    nixpkgs over time, maybe you could become one of them?
+  </p>
+
+  <h3>
+    <a href="#where-is-the-source-code-for-this" aria-hidden="true" class="anchor" id="where-is-the-source-code-for-this"></a>
+    Where is the source code for this?
+  </h3>
+
+  <p>
+    Nixery lives in the <a href="https://cs.tvl.fyi/depot/-/tree/tools/nixery">TVL
+      monorepo</a>. All development happens there and follows
+      the <a href="https://cs.tvl.fyi/depot/-/blob/docs/CONTRIBUTING.md">TVL contribution
+      guidelines</a>.
+  </p>
+
+  <p>
+    We <em>mirror</em> the source code <a href="https://github.com/tazjin/nixery">to
+    Github</a> but do not guarantee that anyone will look at PRs or issues there.
+  </p>
+
+  <hr>
+  <footer>
+    <p class="footer">
+      <a class="uncoloured-link" href="https://at.tvl.fyi/?q=//tools/nixery">code</a>
+      |
+      <a class="uncoloured-link" href="https://cl.tvl.fyi/q/file:%2522%255Etools/nixery/.*%2522">reviews</a>
+      |
+      <a class="uncoloured-link" href="https://b.tvl.fyi/">bugs</a>
+    </p>
+    <p class="lod">ಠ_ಠ</p>
+  </footer>
+
+  <script>
+    /* Replace the hostnames above with the one at which this page runs. */
+    let hostname = window.location.hostname;
+    if (hostname != '') {
+        for (span of document.getElementsByClassName("registry-hostname")) {
+            span.textContent = hostname;
+        }
+    }
+  </script>
+</body>
diff --git a/tools/nixery/web/nixery-logo.png b/tools/nixery/web/nixery-logo.png
new file mode 100644
index 000000000000..fcf77df3d6a9
--- /dev/null
+++ b/tools/nixery/web/nixery-logo.png
Binary files differdiff --git a/tools/nsfv-setup/default.nix b/tools/nsfv-setup/default.nix
new file mode 100644
index 000000000000..1e353e32697b
--- /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 000000000000..b472b746ff14
--- /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 000000000000..d38ca0b5d8be
--- /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" ];
+    phase = "release";
+
+    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 000000000000..5f87d2f27102
--- /dev/null
+++ b/tools/rust-crates-advisory/OWNERS
@@ -0,0 +1,2 @@
+Profpatsch
+sterni
diff --git a/tools/rust-crates-advisory/default.nix b/tools/rust-crates-advisory/default.nix
new file mode 100644
index 000000000000..da7eb7544703
--- /dev/null
+++ b/tools/rust-crates-advisory/default.nix
@@ -0,0 +1,121 @@
+{ depot, pkgs, lib, ... }:
+
+let
+
+  bins =
+    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" ]
+  ;
+
+  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);
+
+  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 = pkgs.writers.writeBash "tree-lock-file-report" ''
+    set -euo pipefail
+    status=0
+
+    root="''${1:-.}"
+
+    # Find prints the found lockfiles as <DEPOT ROOT>\t<LOCKFILE DIR>\t<LOCKFILE PATH>\0
+    while IFS=$'\t' read -r -d $'\0' entryPoint dir lockFile; do
+      label="$(printf '%s' "$dir" | "${bins.sed}" "s|^$entryPoint|/|")"
+      "${lock-file-report}" "$label" "$lockFile" || status=1
+    done < <("${bins.find}" "$root" -type f -name Cargo.lock -printf '%H\t%h\t%p\0' )
+
+    exit $status
+  '';
+
+  depot-rust-crates-advisory-report = pkgs.writers.writeBash "depot-advisory-report" ''
+    set -eu
+    status=0
+
+    "${lock-file-report}" "//third_party/rust-crates" "${our-crates-lock-file}" || status=1
+    "${tree-lock-file-report}" || status=1
+
+    exit $status
+  '';
+
+  buildkiteReportStep =
+    { command
+    , context ? null
+    , style ? "warning"
+    }:
+    let
+      commandName = depot.nix.utils.storePathName (builtins.head command);
+    in
+
+    pkgs.writers.writeBash "buildkite-report-${commandName}" ''
+      set -uo pipefail
+
+      report="$(${lib.escapeShellArgs command})"
+
+      if test $? -ne 0; then
+         printf "%s" "$report" | \
+         buildkite-agent annotate ${
+           lib.escapeShellArgs ([
+             "--style"
+             style
+           ] ++ lib.optionals (context != null) [
+             "--context"
+             context
+           ])
+         }
+      fi
+    '';
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    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 = buildkiteReportStep {
+        command = [ depot-rust-crates-advisory-report ];
+        style = "warning";
+        context = "depot-crate-advisories";
+      };
+    };
+  };
+}
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 000000000000..d42ff6e55c79
--- /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/tvix/.gitignore b/tvix/.gitignore
new file mode 100644
index 000000000000..93233dccf4d3
--- /dev/null
+++ b/tvix/.gitignore
@@ -0,0 +1,4 @@
+/target
+/result-*
+/result
+target
diff --git a/tvix/.vscode/extensions.json b/tvix/.vscode/extensions.json
new file mode 100644
index 000000000000..07405500414a
--- /dev/null
+++ b/tvix/.vscode/extensions.json
@@ -0,0 +1,8 @@
+{
+    "recommendations": [
+        "rust-lang.rust-analyzer"
+    ],
+    "unwantedRecommendations": [
+        "rust-lang.rust"
+    ]
+}
diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock
new file mode 100644
index 000000000000..4716e0e66067
--- /dev/null
+++ b/tvix/Cargo.lock
@@ -0,0 +1,3104 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstream"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is-terminal",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.71"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
+
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
+[[package]]
+name = "arrayvec"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "axum"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "bit-set"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+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 = "bitmaps"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703642b98a00b3b90513279a8ede3fcfa479c126c5fb46e78f3051522f021403"
+
+[[package]]
+name = "blake3"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "cc",
+ "cfg-if",
+ "constant_time_eq",
+ "digest 0.10.6",
+ "rayon",
+]
+
+[[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 0.12.4",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array 0.14.7",
+]
+
+[[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 = "bumpalo"
+version = "3.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
+
+[[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 = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+dependencies = [
+ "jobserver",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "ciborium"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "3.2.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
+dependencies = [
+ "bitflags",
+ "clap_lex 0.2.4",
+ "indexmap",
+ "textwrap",
+]
+
+[[package]]
+name = "clap"
+version = "4.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "bitflags",
+ "clap_lex 0.4.1",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4"
+dependencies = [
+ "heck",
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1"
+
+[[package]]
+name = "clipboard-win"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
+dependencies = [
+ "error-code",
+ "str-buf",
+ "winapi",
+]
+
+[[package]]
+name = "codemap"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
+
+[[package]]
+name = "codemap-diagnostic"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ba0e6be8e2774e750f9e90625b490249715bece38a12f9d09e82477caba5028"
+dependencies = [
+ "atty",
+ "codemap",
+ "termcolor",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13418e745008f7349ec7e449155f419a61b92b58a99cc3616942b926825ec76b"
+
+[[package]]
+name = "count-write"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced507ab50aa0123e2c54db8b5f44fdfee04b1c93744d69e924307945fe57a85"
+
+[[package]]
+name = "countme"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
+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 = "criterion"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb"
+dependencies = [
+ "anes",
+ "atty",
+ "cast",
+ "ciborium",
+ "clap 3.2.25",
+ "criterion-plot",
+ "itertools",
+ "lazy_static",
+ "num-traits",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array 0.14.7",
+ "typenum",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote 1.0.26",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
+
+[[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
+name = "digest"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
+dependencies = [
+ "generic-array 0.12.4",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common",
+ "subtle",
+]
+
+[[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-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[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 = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "endian-type"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
+
+[[package]]
+name = "errno"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "error-code"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
+dependencies = [
+ "libc",
+ "str-buf",
+]
+
+[[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.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fd-lock"
+version = "3.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ae6b3d9530211fb3b12a95374b8b0823be812f53d09e18c5675c0146b09642"
+dependencies = [
+ "cfg-if",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+
+[[package]]
+name = "futures"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "genawaiter"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
+dependencies = [
+ "genawaiter-macro",
+]
+
+[[package]]
+name = "genawaiter-macro"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
+
+[[package]]
+name = "generic-array"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "h2"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
+[[package]]
+name = "hex"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77"
+
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[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.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+dependencies = [
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tokio-io-timeout",
+]
+
+[[package]]
+name = "imbl"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2806b69cd9f4664844027b64465eacb444c67c1db9c778e341adff0c25cdb0d"
+dependencies = [
+ "bitmaps",
+ "imbl-sized-chunks",
+ "proptest",
+ "rand_core 0.6.4",
+ "rand_xoshiro",
+ "serde",
+ "version_check",
+]
+
+[[package]]
+name = "imbl-sized-chunks"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6957ea0b2541c5ca561d3ef4538044af79f8a05a1eb3a3b148936aaceaa1076"
+dependencies = [
+ "bitmaps",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+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 = "io-lifetimes"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
+
+[[package]]
+name = "jobserver"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+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 = "lexical-core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46"
+dependencies = [
+ "lexical-parse-float",
+ "lexical-parse-integer",
+ "lexical-util",
+ "lexical-write-float",
+ "lexical-write-integer",
+]
+
+[[package]]
+name = "lexical-parse-float"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f"
+dependencies = [
+ "lexical-parse-integer",
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-parse-integer"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9"
+dependencies = [
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-util"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc"
+dependencies = [
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-write-float"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862"
+dependencies = [
+ "lexical-util",
+ "lexical-write-integer",
+ "static_assertions",
+]
+
+[[package]]
+name = "lexical-write-integer"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446"
+dependencies = [
+ "lexical-util",
+ "static_assertions",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.143"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edc207893e85c5d6be840e969b496b53d94cec8be2d501b214f50daa97fa8024"
+
+[[package]]
+name = "libm"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
+
+[[package]]
+name = "lock_api"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+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 = "matchit"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "memoffset"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mio"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "multimap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
+
+[[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.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nix-cli"
+version = "0.1.0"
+dependencies = [
+ "clap 4.2.7",
+ "tempfile",
+]
+
+[[package]]
+name = "nix-compat"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "data-encoding",
+ "glob",
+ "serde",
+ "serde_json",
+ "sha2 0.10.6",
+ "test-case",
+ "test-generator",
+ "thiserror",
+]
+
+[[package]]
+name = "nom8"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
+dependencies = [
+ "hermit-abi 0.2.6",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
+
+[[package]]
+name = "oorandom"
+version = "11.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
+
+[[package]]
+name = "opaque-debug"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
+
+[[package]]
+name = "os_str_bytes"
+version = "6.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267"
+
+[[package]]
+name = "output_vt100"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "path-clean"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "petgraph"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+]
+
+[[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 = "plotters"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "pretty_assertions"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755"
+dependencies = [
+ "ctor",
+ "diff",
+ "output_vt100",
+ "yansi",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "syn 1.0.109",
+]
+
+[[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 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+ "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 1.0.56",
+ "quote 1.0.26",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proptest"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70"
+dependencies = [
+ "bit-set",
+ "bitflags",
+ "byteorder",
+ "lazy_static",
+ "num-traits",
+ "quick-error 2.0.1",
+ "rand 0.8.5",
+ "rand_chacha",
+ "rand_xorshift",
+ "regex-syntax 0.6.29",
+ "rusty-fork",
+ "tempfile",
+ "unarray",
+]
+
+[[package]]
+name = "prost"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools",
+ "lazy_static",
+ "log",
+ "multimap",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 1.0.109",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
+dependencies = [
+ "prost",
+]
+
+[[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 = "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.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
+dependencies = [
+ "proc-macro2 1.0.56",
+]
+
+[[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.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"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core 0.6.4",
+]
+
+[[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.4",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+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 0.6.4",
+]
+
+[[package]]
+name = "rand_xoshiro"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rayon"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
+[[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.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+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 0.2.16",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax 0.7.1",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
+
+[[package]]
+name = "regex-syntax"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
+
+[[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 = "rnix"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb35cedbeb70e0ccabef2a31bcff0aebd114f19566086300b8f42c725fc2cb5f"
+dependencies = [
+ "rowan",
+]
+
+[[package]]
+name = "rowan"
+version = "0.15.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64449cfef9483a475ed56ae30e2da5ee96448789fb2aa240a04beb6a055078bf"
+dependencies = [
+ "countme",
+ "hashbrown",
+ "memoffset",
+ "rustc-hash",
+ "text-size",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustix"
+version = "0.37.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
+
+[[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 = "rustyline"
+version = "10.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1e83c32c3f3c33b08496e0d1df9ea8c64d39adb8eb36a1ebb1440c690697aef"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "clipboard-win",
+ "dirs-next",
+ "fd-lock",
+ "libc",
+ "log",
+ "memchr",
+ "nix",
+ "radix_trie",
+ "scopeguard",
+ "unicode-segmentation",
+ "unicode-width",
+ "utf8parse",
+ "winapi",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
+[[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.162"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.162"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer 0.7.3",
+ "digest 0.8.1",
+ "fake-simd",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69"
+dependencies = [
+ "block-buffer 0.7.3",
+ "digest 0.8.1",
+ "fake-simd",
+ "opaque-debug",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest 0.10.6",
+]
+
+[[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 = "slab"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot",
+ "zstd",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "smol_str"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "ssri"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9cec0d388f39fbe79d7aa600e8d38053bf97b1bc8d350da7c0ba800d0f423f2"
+dependencies = [
+ "base64 0.10.1",
+ "digest 0.8.1",
+ "hex",
+ "serde",
+ "sha-1",
+ "sha2 0.8.2",
+ "thiserror",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "str-buf"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0"
+
+[[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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "104842d6278bf64aa9d2f182ba4bde31e8aec7a131d29b7f444bb9b344a09e2a"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "structmeta-derive",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "structmeta-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24420be405b590e2d746d83b01f09af673270cf80e9b003a5fa7b651c58c7d93"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[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",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "tabwriter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36205cfc997faadcc4b0b87aaef3fbedafe20d38d4959a7ca6ff803564051111"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "tempdir"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
+dependencies = [
+ "rand 0.4.6",
+ "remove_dir_all",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall 0.3.5",
+ "rustix",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "test-case"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21d6cf5a7dffb3f9dceec8e6b8ca528d9bd71d36c9f074defb548ce161f598c0"
+dependencies = [
+ "test-case-macros",
+]
+
+[[package]]
+name = "test-case-macros"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e45b7bf6e19353ddd832745c8fcf77a17a93171df7151187f26623f2b75b5b26"
+dependencies = [
+ "cfg-if",
+ "proc-macro-error",
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "test-generator"
+version = "0.3.0"
+source = "git+https://github.com/JamesGuthrie/test-generator.git?rev=82e799979980962aec1aa324ec6e0e4cad781f41#82e799979980962aec1aa324ec6e0e4cad781f41"
+dependencies = [
+ "glob",
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
+[[package]]
+name = "test-strategy"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62d6408d1406657be2f9d1701fbae379331d30d2f6e92050710edb0d34eeb480"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "structmeta",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "text-size"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a"
+
+[[package]]
+name = "textwrap"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+
+[[package]]
+name = "thiserror"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tokio"
+version = "1.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "tokio-io-timeout"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
+dependencies = [
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "toml"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b"
+dependencies = [
+ "indexmap",
+ "nom8",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+]
+
+[[package]]
+name = "tonic"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f219fad3b929bef19b1f86fbc0358d35daed8f2cac972037ac0dc10bbb8d5fb"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64 0.13.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-timeout",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "prost-derive",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+ "tracing-futures",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bf5e9b9c0f7e0a7c027dcfaba7b2c60816c7049171f679d99ee2ff65d0de8c4"
+dependencies = [
+ "prettyplease",
+ "proc-macro2 1.0.56",
+ "prost-build",
+ "quote 1.0.26",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "tonic-mock"
+version = "0.1.0"
+source = "git+https://github.com/brainrake/tonic-mock?branch=bump-dependencies#ec1a15510875de99d709d684190db5d9beab175e"
+dependencies = [
+ "bytes",
+ "futures",
+ "http",
+ "http-body",
+ "prost",
+ "tonic",
+]
+
+[[package]]
+name = "tonic-reflection"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0455f730d540a1484bffc3c55c94100b18a662597b982c2e9073f2c55c602616"
+dependencies = [
+ "bytes",
+ "prost",
+ "prost-types",
+ "tokio",
+ "tokio-stream",
+ "tonic",
+ "tonic-build",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap",
+ "pin-project",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 2.0.15",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
+[[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.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
+dependencies = [
+ "nu-ansi-term",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
+[[package]]
+name = "tvix-cli"
+version = "0.1.0"
+dependencies = [
+ "clap 4.2.7",
+ "data-encoding",
+ "dirs",
+ "nix-compat",
+ "rustyline",
+ "smol_str",
+ "ssri",
+ "thiserror",
+ "tvix-eval",
+ "tvix-store",
+ "wu-manber",
+]
+
+[[package]]
+name = "tvix-eval"
+version = "0.1.0"
+dependencies = [
+ "codemap",
+ "codemap-diagnostic",
+ "criterion",
+ "dirs",
+ "genawaiter",
+ "imbl",
+ "itertools",
+ "lazy_static",
+ "lexical-core",
+ "path-clean",
+ "pretty_assertions",
+ "proptest",
+ "regex",
+ "rnix",
+ "rowan",
+ "serde",
+ "serde_json",
+ "smol_str",
+ "tabwriter",
+ "tempdir",
+ "test-generator",
+ "test-strategy",
+ "toml",
+ "tvix-eval-builtin-macros",
+ "xml-rs",
+]
+
+[[package]]
+name = "tvix-eval-builtin-macros"
+version = "0.0.1"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+ "tvix-eval",
+]
+
+[[package]]
+name = "tvix-serde"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "tvix-eval",
+]
+
+[[package]]
+name = "tvix-store"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "blake3",
+ "bytes",
+ "clap 4.2.7",
+ "count-write",
+ "data-encoding",
+ "futures",
+ "lazy_static",
+ "nix-compat",
+ "prost",
+ "prost-build",
+ "rayon",
+ "serde_json",
+ "sha2 0.10.6",
+ "sled",
+ "smol_str",
+ "tempfile",
+ "test-case",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tonic",
+ "tonic-build",
+ "tonic-mock",
+ "tonic-reflection",
+ "tower",
+ "tracing",
+ "tracing-subscriber",
+ "tvix-eval",
+ "walkdir",
+]
+
+[[package]]
+name = "typenum"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+
+[[package]]
+name = "unarray"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "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.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote 1.0.26",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2 1.0.56",
+ "quote 1.0.26",
+ "syn 1.0.109",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "web-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "which"
+version = "4.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
+[[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.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "wu-manber"
+version = "0.1.0"
+source = "git+https://github.com/tvlfyi/wu-manber.git#e77628cafcf45d41b9e455be86a6b1b9f46b2092"
+
+[[package]]
+name = "xml-rs"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "699d0104bcdd7e7af6d093d6c6e2d0c479b8a129ee0d1023b31d2e0c71bfdda2"
+
+[[package]]
+name = "yansi"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
+
+[[package]]
+name = "zstd"
+version = "0.9.2+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "4.1.3+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79"
+dependencies = [
+ "libc",
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "1.6.2+zstd.1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f"
+dependencies = [
+ "cc",
+ "libc",
+]
diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix
new file mode 100644
index 000000000000..332c2834f809
--- /dev/null
+++ b/tvix/Cargo.nix
@@ -0,0 +1,10624 @@
+# This file was @generated by crate2nix 0.10.0 with the command:
+#   "generate"
+# See https://github.com/kolloch/crate2nix for more info.
+
+{ nixpkgs ? <nixpkgs>
+, pkgs ? import nixpkgs { config = { }; }
+, lib ? pkgs.lib
+, stdenv ? pkgs.stdenv
+, buildRustCrateForPkgs ? pkgs: pkgs.buildRustCrate
+  # This is used as the `crateOverrides` argument for `buildRustCrate`.
+, defaultCrateOverrides ? pkgs.defaultCrateOverrides
+  # The features to enable for the root_crate or the workspace_members.
+, rootFeatures ? [ "default" ]
+  # If true, throw errors instead of issueing deprecation warnings.
+, strictDeprecation ? false
+  # Used for conditional compilation based on CPU feature detection.
+, targetFeatures ? [ ]
+  # Whether to perform release builds: longer compile times, faster binaries.
+, release ? true
+  # Additional crate2nix configuration if it exists.
+, crateConfig ? if builtins.pathExists ./crate-config.nix
+  then pkgs.callPackage ./crate-config.nix { }
+  else { }
+}:
+
+rec {
+  #
+  # "public" attributes that we attempt to keep stable with new versions of crate2nix.
+  #
+
+
+  # Refer your crate build derivation by name here.
+  # You can override the features with
+  # workspaceMembers."${crateName}".build.override { features = [ "default" "feature1" ... ]; }.
+  workspaceMembers = {
+    "nix-cli" = rec {
+      packageId = "nix-cli";
+      build = internal.buildRustCrateWithFeatures {
+        packageId = "nix-cli";
+      };
+
+      # Debug support which might change between releases.
+      # File a bug if you depend on any for non-debug work!
+      debug = internal.debugCrate { inherit packageId; };
+    };
+    "nix-compat" = rec {
+      packageId = "nix-compat";
+      build = internal.buildRustCrateWithFeatures {
+        packageId = "nix-compat";
+      };
+
+      # Debug support which might change between releases.
+      # File a bug if you depend on any for non-debug work!
+      debug = internal.debugCrate { inherit packageId; };
+    };
+    "tvix-cli" = rec {
+      packageId = "tvix-cli";
+      build = internal.buildRustCrateWithFeatures {
+        packageId = "tvix-cli";
+      };
+
+      # Debug support which might change between releases.
+      # File a bug if you depend on any for non-debug work!
+      debug = internal.debugCrate { inherit packageId; };
+    };
+    "tvix-eval" = rec {
+      packageId = "tvix-eval";
+      build = internal.buildRustCrateWithFeatures {
+        packageId = "tvix-eval";
+      };
+
+      # Debug support which might change between releases.
+      # File a bug if you depend on any for non-debug work!
+      debug = internal.debugCrate { inherit packageId; };
+    };
+    "tvix-eval-builtin-macros" = rec {
+      packageId = "tvix-eval-builtin-macros";
+      build = internal.buildRustCrateWithFeatures {
+        packageId = "tvix-eval-builtin-macros";
+      };
+
+      # Debug support which might change between releases.
+      # File a bug if you depend on any for non-debug work!
+      debug = internal.debugCrate { inherit packageId; };
+    };
+    "tvix-serde" = rec {
+      packageId = "tvix-serde";
+      build = internal.buildRustCrateWithFeatures {
+        packageId = "tvix-serde";
+      };
+
+      # Debug support which might change between releases.
+      # File a bug if you depend on any for non-debug work!
+      debug = internal.debugCrate { inherit packageId; };
+    };
+    "tvix-store" = rec {
+      packageId = "tvix-store";
+      build = internal.buildRustCrateWithFeatures {
+        packageId = "tvix-store";
+      };
+
+      # Debug support which might change between releases.
+      # File a bug if you depend on any for non-debug work!
+      debug = internal.debugCrate { inherit packageId; };
+    };
+  };
+
+  # A derivation that joins the outputs of all workspace members together.
+  allWorkspaceMembers = pkgs.symlinkJoin {
+    name = "all-workspace-members";
+    paths =
+      let members = builtins.attrValues workspaceMembers;
+      in builtins.map (m: m.build) members;
+  };
+
+  #
+  # "internal" ("private") attributes that may change in every new version of crate2nix.
+  #
+
+  internal = rec {
+    # Build and dependency information for crates.
+    # Many of the fields are passed one-to-one to buildRustCrate.
+    #
+    # Noteworthy:
+    # * `dependencies`/`buildDependencies`: similar to the corresponding fields for buildRustCrate.
+    #   but with additional information which is used during dependency/feature resolution.
+    # * `resolvedDependencies`: the selected default features reported by cargo - only included for debugging.
+    # * `devDependencies` as of now not used by `buildRustCrate` but used to
+    #   inject test dependencies into the build
+
+    crates = {
+      "aho-corasick" = rec {
+        crateName = "aho-corasick";
+        version = "1.0.1";
+        edition = "2021";
+        sha256 = "014ddyrlbwg18m74fa52wrfik8y3pzhwqg811yvsyc8cjb70iz37";
+        libName = "aho_corasick";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "memchr";
+            packageId = "memchr";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "std" "perf-literal" ];
+          "logging" = [ "dep:log" ];
+          "perf-literal" = [ "dep:memchr" ];
+          "std" = [ "memchr?/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "perf-literal" "std" ];
+      };
+      "anes" = rec {
+        crateName = "anes";
+        version = "0.1.6";
+        edition = "2018";
+        sha256 = "16bj1ww1xkwzbckk32j2pnbn5vk6wgsl3q4p3j9551xbcarwnijb";
+        authors = [
+          "Robert Vojta <rvojta@me.com>"
+        ];
+        features = {
+          "bitflags" = [ "dep:bitflags" ];
+          "parser" = [ "bitflags" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "anstream" = rec {
+        crateName = "anstream";
+        version = "0.3.2";
+        edition = "2021";
+        sha256 = "0qzinx9c8zfq3xqpxzmlv6nrm3ymccr4n8gffkdmj31p50v4za0c";
+        dependencies = [
+          {
+            name = "anstyle";
+            packageId = "anstyle";
+          }
+          {
+            name = "anstyle-parse";
+            packageId = "anstyle-parse";
+          }
+          {
+            name = "anstyle-query";
+            packageId = "anstyle-query";
+            optional = true;
+          }
+          {
+            name = "anstyle-wincon";
+            packageId = "anstyle-wincon";
+            optional = true;
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "colorchoice";
+            packageId = "colorchoice";
+            optional = true;
+          }
+          {
+            name = "is-terminal";
+            packageId = "is-terminal";
+            optional = true;
+          }
+          {
+            name = "utf8parse";
+            packageId = "utf8parse";
+          }
+        ];
+        features = {
+          "auto" = [ "dep:anstyle-query" "dep:colorchoice" "dep:is-terminal" ];
+          "default" = [ "auto" "wincon" ];
+          "wincon" = [ "dep:anstyle-wincon" ];
+        };
+        resolvedDefaultFeatures = [ "auto" "default" "wincon" ];
+      };
+      "anstyle" = rec {
+        crateName = "anstyle";
+        version = "1.0.0";
+        edition = "2021";
+        sha256 = "0zbazbfqs4mfw93573f61iy8c78vbbv824m3w206bbljpy39mva1";
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "anstyle-parse" = rec {
+        crateName = "anstyle-parse";
+        version = "0.2.0";
+        edition = "2021";
+        sha256 = "1vjprf080adyxxpls9iwwny3g7irawfns9s2cj9ngq28dqhzsrg7";
+        dependencies = [
+          {
+            name = "utf8parse";
+            packageId = "utf8parse";
+            optional = true;
+          }
+        ];
+        features = {
+          "core" = [ "dep:arrayvec" ];
+          "default" = [ "utf8" ];
+          "utf8" = [ "dep:utf8parse" ];
+        };
+        resolvedDefaultFeatures = [ "default" "utf8" ];
+      };
+      "anstyle-query" = rec {
+        crateName = "anstyle-query";
+        version = "1.0.0";
+        edition = "2021";
+        sha256 = "0js9bgpqz21g0p2nm350cba1d0zfyixsma9lhyycic5sw55iv8aw";
+        dependencies = [
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_System_Console" "Win32_Foundation" ];
+          }
+        ];
+
+      };
+      "anstyle-wincon" = rec {
+        crateName = "anstyle-wincon";
+        version = "1.0.1";
+        edition = "2021";
+        sha256 = "12714vwjf4c1wm3qf49m5vmd93qvq2nav6zpjc0bxbh3ayjby2hq";
+        dependencies = [
+          {
+            name = "anstyle";
+            packageId = "anstyle";
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_System_Console" "Win32_Foundation" ];
+          }
+        ];
+
+      };
+      "anyhow" = rec {
+        crateName = "anyhow";
+        version = "1.0.71";
+        edition = "2018";
+        sha256 = "1f6rm4c9nlp0wazm80wlw45zpmb48nv24x2227zyidz0y0c0czcw";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        features = {
+          "backtrace" = [ "dep:backtrace" ];
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "arrayref" = rec {
+        crateName = "arrayref";
+        version = "0.3.7";
+        edition = "2015";
+        sha256 = "0ia5ndyxqkzdymqr4ls53jdmajf09adjimg5kvw65kkprg930jbb";
+        authors = [
+          "David Roundy <roundyd@physics.oregonstate.edu>"
+        ];
+
+      };
+      "arrayvec" = rec {
+        crateName = "arrayvec";
+        version = "0.7.2";
+        edition = "2018";
+        sha256 = "1mjl8jjqxpl0x7sm9cij61cppi7yi38cdrd1l8zjw7h7qxk2v9cd";
+        authors = [
+          "bluss"
+        ];
+        features = {
+          "default" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "async-stream" = rec {
+        crateName = "async-stream";
+        version = "0.3.5";
+        edition = "2018";
+        sha256 = "0l8sjq1rylkb1ak0pdyjn83b3k6x36j22myngl4sqqgg7whdsmnd";
+        authors = [
+          "Carl Lerche <me@carllerche.com>"
+        ];
+        dependencies = [
+          {
+            name = "async-stream-impl";
+            packageId = "async-stream-impl";
+          }
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+        ];
+
+      };
+      "async-stream-impl" = rec {
+        crateName = "async-stream-impl";
+        version = "0.3.5";
+        edition = "2018";
+        sha256 = "14q179j4y8p2z1d0ic6aqgy9fhwz8p9cai1ia8kpw4bw7q12mrhn";
+        procMacro = true;
+        authors = [
+          "Carl Lerche <me@carllerche.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+            features = [ "full" "visit-mut" ];
+          }
+        ];
+
+      };
+      "async-trait" = rec {
+        crateName = "async-trait";
+        version = "0.1.68";
+        edition = "2018";
+        sha256 = "0hp8ysdjr8c43avm7bkj73cd22ra3dpzag82bjyyj6qn5a7xvk5r";
+        procMacro = true;
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+            features = [ "full" "visit-mut" ];
+          }
+        ];
+
+      };
+      "atty" = rec {
+        crateName = "atty";
+        version = "0.2.14";
+        edition = "2015";
+        sha256 = "1s7yslcs6a28c5vz7jwj63lkfgyx8mx99fdirlhi9lbhhzhrpcyr";
+        authors = [
+          "softprops <d.tangren@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "hermit-abi";
+            packageId = "hermit-abi 0.1.19";
+            target = { target, features }: ("hermit" == target."os");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            usesDefaultFeatures = false;
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "consoleapi" "processenv" "minwinbase" "minwindef" "winbase" ];
+          }
+        ];
+
+      };
+      "autocfg" = rec {
+        crateName = "autocfg";
+        version = "1.1.0";
+        edition = "2015";
+        sha256 = "1ylp3cb47ylzabimazvbz9ms6ap784zhb6syaz6c1jqpmcmq0s6l";
+        authors = [
+          "Josh Stone <cuviper@gmail.com>"
+        ];
+
+      };
+      "axum" = rec {
+        crateName = "axum";
+        version = "0.6.18";
+        edition = "2021";
+        sha256 = "0ffzv20n4f68qa7d9cp4am0l7np0wxp5ixkv3lf3694i4mwmj5zq";
+        dependencies = [
+          {
+            name = "async-trait";
+            packageId = "async-trait";
+          }
+          {
+            name = "axum-core";
+            packageId = "axum-core";
+          }
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+            features = [ "alloc" ];
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "http-body";
+            packageId = "http-body";
+          }
+          {
+            name = "hyper";
+            packageId = "hyper";
+            features = [ "stream" ];
+          }
+          {
+            name = "itoa";
+            packageId = "itoa";
+          }
+          {
+            name = "matchit";
+            packageId = "matchit";
+          }
+          {
+            name = "memchr";
+            packageId = "memchr";
+          }
+          {
+            name = "mime";
+            packageId = "mime";
+          }
+          {
+            name = "percent-encoding";
+            packageId = "percent-encoding";
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+          }
+          {
+            name = "sync_wrapper";
+            packageId = "sync_wrapper";
+          }
+          {
+            name = "tower";
+            packageId = "tower";
+            usesDefaultFeatures = false;
+            features = [ "util" ];
+          }
+          {
+            name = "tower-layer";
+            packageId = "tower-layer";
+          }
+          {
+            name = "tower-service";
+            packageId = "tower-service";
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "rustversion";
+            packageId = "rustversion";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "rustversion";
+            packageId = "rustversion";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "derive" ];
+          }
+          {
+            name = "tower";
+            packageId = "tower";
+            rename = "tower";
+            features = [ "util" "timeout" "limit" "load-shed" "steer" "filter" ];
+          }
+        ];
+        features = {
+          "__private_docs" = [ "tower/full" "dep:tower-http" ];
+          "default" = [ "form" "http1" "json" "matched-path" "original-uri" "query" "tokio" "tower-log" ];
+          "form" = [ "dep:serde_urlencoded" ];
+          "headers" = [ "dep:headers" ];
+          "http1" = [ "hyper/http1" ];
+          "http2" = [ "hyper/http2" ];
+          "json" = [ "dep:serde_json" "dep:serde_path_to_error" ];
+          "macros" = [ "dep:axum-macros" ];
+          "multipart" = [ "dep:multer" ];
+          "query" = [ "dep:serde_urlencoded" ];
+          "tokio" = [ "dep:tokio" "hyper/server" "hyper/tcp" "hyper/runtime" "tower/make" ];
+          "tower-log" = [ "tower/log" ];
+          "tracing" = [ "dep:tracing" "axum-core/tracing" ];
+          "ws" = [ "tokio" "dep:tokio-tungstenite" "dep:sha1" "dep:base64" ];
+        };
+      };
+      "axum-core" = rec {
+        crateName = "axum-core";
+        version = "0.3.4";
+        edition = "2021";
+        sha256 = "0b1d9nkqb8znaba4qqzxzc968qwj4ybn4vgpyz9lz4a7l9vsb7vm";
+        dependencies = [
+          {
+            name = "async-trait";
+            packageId = "async-trait";
+          }
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+            features = [ "alloc" ];
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "http-body";
+            packageId = "http-body";
+          }
+          {
+            name = "mime";
+            packageId = "mime";
+          }
+          {
+            name = "tower-layer";
+            packageId = "tower-layer";
+          }
+          {
+            name = "tower-service";
+            packageId = "tower-service";
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "rustversion";
+            packageId = "rustversion";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+            features = [ "alloc" ];
+          }
+        ];
+        features = {
+          "__private_docs" = [ "dep:tower-http" ];
+          "tracing" = [ "dep:tracing" ];
+        };
+      };
+      "base64 0.10.1" = rec {
+        crateName = "base64";
+        version = "0.10.1";
+        edition = "2015";
+        sha256 = "13k6bvd3n6dm7jqn9x918w65dd9xhx454bqphbnv0bkd6n9dj98b";
+        authors = [
+          "Alice Maz <alice@alicemaz.com>"
+          "Marshall Pierce <marshall@mpierce.org>"
+        ];
+        dependencies = [
+          {
+            name = "byteorder";
+            packageId = "byteorder";
+          }
+        ];
+
+      };
+      "base64 0.13.1" = rec {
+        crateName = "base64";
+        version = "0.13.1";
+        edition = "2018";
+        sha256 = "1s494mqmzjb766fy1kqlccgfg2sdcjb6hzbvzqv2jw65fdi5h6wy";
+        authors = [
+          "Alice Maz <alice@alicemaz.com>"
+          "Marshall Pierce <marshall@mpierce.org>"
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "bit-set" = rec {
+        crateName = "bit-set";
+        version = "0.5.3";
+        edition = "2015";
+        sha256 = "1wcm9vxi00ma4rcxkl3pzzjli6ihrpn9cfdi0c5b4cvga2mxs007";
+        authors = [
+          "Alexis Beingessner <a.beingessner@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "bit-vec";
+            packageId = "bit-vec";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "std" = [ "bit-vec/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "bit-vec" = rec {
+        crateName = "bit-vec";
+        version = "0.6.3";
+        edition = "2015";
+        sha256 = "1ywqjnv60cdh1slhz67psnp422md6jdliji6alq0gmly2xm9p7rl";
+        authors = [
+          "Alexis Beingessner <a.beingessner@gmail.com>"
+        ];
+        features = {
+          "default" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+          "serde_no_std" = [ "serde/alloc" ];
+          "serde_std" = [ "std" "serde/std" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "bitflags" = rec {
+        crateName = "bitflags";
+        version = "1.3.2";
+        edition = "2018";
+        sha256 = "12ki6w8gn1ldq7yz9y680llwk5gmrhrzszaa17g1sbrw2r2qvwxy";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "rustc-dep-of-std" = [ "core" "compiler_builtins" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "bitmaps" = rec {
+        crateName = "bitmaps";
+        version = "3.2.0";
+        edition = "2021";
+        sha256 = "00ql08pm4l9hizkldyy54v0pk96g7zg8x6i72c2vkcq0iawl4dkh";
+        authors = [
+          "Bodil Stokke <bodil@bodil.org>"
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "blake3" = rec {
+        crateName = "blake3";
+        version = "1.3.3";
+        edition = "2018";
+        sha256 = "1vyckzpfq46dkxyvy12gkx4nddr5g93alh38i1ka8i4mm1l29bj2";
+        authors = [
+          "Jack O'Connor <oconnor663@gmail.com>"
+          "Samuel Neves"
+        ];
+        dependencies = [
+          {
+            name = "arrayref";
+            packageId = "arrayref";
+          }
+          {
+            name = "arrayvec";
+            packageId = "arrayvec";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "constant_time_eq";
+            packageId = "constant_time_eq";
+          }
+          {
+            name = "digest";
+            packageId = "digest 0.10.6";
+            optional = true;
+            features = [ "mac" ];
+          }
+          {
+            name = "rayon";
+            packageId = "rayon";
+            optional = true;
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "cc";
+            packageId = "cc";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "digest" = [ "dep:digest" ];
+          "rayon" = [ "dep:rayon" ];
+          "std" = [ "digest/std" ];
+          "traits-preview" = [ "digest" ];
+        };
+        resolvedDefaultFeatures = [ "default" "digest" "rayon" "std" ];
+      };
+      "block-buffer 0.10.4" = rec {
+        crateName = "block-buffer";
+        version = "0.10.4";
+        edition = "2018";
+        sha256 = "0w9sa2ypmrsqqvc20nhwr75wbb5cjr4kkyhpjm1z1lv2kdicfy1h";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "generic-array";
+            packageId = "generic-array 0.14.7";
+          }
+        ];
+
+      };
+      "block-buffer 0.7.3" = rec {
+        crateName = "block-buffer";
+        version = "0.7.3";
+        edition = "2015";
+        sha256 = "12v8wizynqin0hqf140kmp9s38q223mp1b0hkqk8j5pk8720v560";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "block-padding";
+            packageId = "block-padding";
+          }
+          {
+            name = "byte-tools";
+            packageId = "byte-tools";
+          }
+          {
+            name = "byteorder";
+            packageId = "byteorder";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "generic-array";
+            packageId = "generic-array 0.12.4";
+          }
+        ];
+
+      };
+      "block-padding" = rec {
+        crateName = "block-padding";
+        version = "0.1.5";
+        edition = "2015";
+        sha256 = "1xbkmysiz23vimd17rnsjpw9bgjxipwfslwyygqlkx4in3dxwygs";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "byte-tools";
+            packageId = "byte-tools";
+          }
+        ];
+
+      };
+      "bumpalo" = rec {
+        crateName = "bumpalo";
+        version = "3.12.1";
+        edition = "2021";
+        sha256 = "1j7hjszs00lgl1ddwg4369f4jh87cbpf1m3xzczz751n0scy274v";
+        authors = [
+          "Nick Fitzgerald <fitzgen@gmail.com>"
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "byte-tools" = rec {
+        crateName = "byte-tools";
+        version = "0.3.1";
+        edition = "2015";
+        sha256 = "1mqi29wsm8njpl51pfwr31wmpzs5ahlcb40wsjyd92l90ixcmdg3";
+        authors = [
+          "RustCrypto Developers"
+        ];
+
+      };
+      "byteorder" = rec {
+        crateName = "byteorder";
+        version = "1.4.3";
+        edition = "2018";
+        sha256 = "0456lv9xi1a5bcm32arknf33ikv76p3fr9yzki4lb2897p2qkh8l";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "bytes" = rec {
+        crateName = "bytes";
+        version = "1.4.0";
+        edition = "2018";
+        sha256 = "1gkh3fk4fm9xv5znlib723h5md5sxsvbd5113sbxff6g1lmgvcl9";
+        authors = [
+          "Carl Lerche <me@carllerche.com>"
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        features = {
+          "default" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "cast" = rec {
+        crateName = "cast";
+        version = "0.3.0";
+        edition = "2018";
+        sha256 = "1dbyngbyz2qkk0jn2sxil8vrz3rnpcj142y184p9l4nbl9radcip";
+        authors = [
+          "Jorge Aparicio <jorge@japaric.io>"
+        ];
+        features = { };
+      };
+      "cc" = rec {
+        crateName = "cc";
+        version = "1.0.79";
+        edition = "2018";
+        crateBin = [ ];
+        sha256 = "07x93b8zbf3xc2dggdd460xlk1wg8lxm6yflwddxj8b15030klsh";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "jobserver";
+            packageId = "jobserver";
+            optional = true;
+          }
+        ];
+        features = {
+          "jobserver" = [ "dep:jobserver" ];
+          "parallel" = [ "jobserver" ];
+        };
+        resolvedDefaultFeatures = [ "jobserver" "parallel" ];
+      };
+      "cfg-if" = rec {
+        crateName = "cfg-if";
+        version = "1.0.0";
+        edition = "2018";
+        sha256 = "1za0vb97n4brpzpv8lsbnzmq5r8f2b0cpqqr0sy8h5bn751xxwds";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "rustc-dep-of-std" = [ "core" "compiler_builtins" ];
+        };
+      };
+      "ciborium" = rec {
+        crateName = "ciborium";
+        version = "0.2.0";
+        edition = "2021";
+        sha256 = "13vqkm88kaq8nvxhaj6qsl0gsc16rqsin014fx5902y6iib3ghdh";
+        authors = [
+          "Nathaniel McCallum <npmccallum@profian.com>"
+        ];
+        dependencies = [
+          {
+            name = "ciborium-io";
+            packageId = "ciborium-io";
+            features = [ "alloc" ];
+          }
+          {
+            name = "ciborium-ll";
+            packageId = "ciborium-ll";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            usesDefaultFeatures = false;
+            features = [ "alloc" "derive" ];
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "std" = [ "ciborium-io/std" "serde/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "ciborium-io" = rec {
+        crateName = "ciborium-io";
+        version = "0.2.0";
+        edition = "2021";
+        sha256 = "0sdkk7l7pqi2nsbm9c6g8im1gb1qdd83l25ja9xwhg07mx9yfv9l";
+        authors = [
+          "Nathaniel McCallum <npmccallum@profian.com>"
+        ];
+        features = {
+          "std" = [ "alloc" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "std" ];
+      };
+      "ciborium-ll" = rec {
+        crateName = "ciborium-ll";
+        version = "0.2.0";
+        edition = "2021";
+        sha256 = "06ygqh33k3hp9r9mma43gf189b6cyq62clk65f4w1q54nni30c11";
+        authors = [
+          "Nathaniel McCallum <npmccallum@profian.com>"
+        ];
+        dependencies = [
+          {
+            name = "ciborium-io";
+            packageId = "ciborium-io";
+          }
+          {
+            name = "half";
+            packageId = "half";
+          }
+        ];
+        features = {
+          "std" = [ "alloc" ];
+        };
+      };
+      "clap 3.2.25" = rec {
+        crateName = "clap";
+        version = "3.2.25";
+        edition = "2021";
+        crateBin = [ ];
+        sha256 = "08vi402vfqmfj9f07c4gl6082qxgf4c9x98pbndcnwbgaszq38af";
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "clap_lex";
+            packageId = "clap_lex 0.2.4";
+          }
+          {
+            name = "indexmap";
+            packageId = "indexmap";
+          }
+          {
+            name = "textwrap";
+            packageId = "textwrap";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "atty" = [ "dep:atty" ];
+          "backtrace" = [ "dep:backtrace" ];
+          "cargo" = [ "once_cell" ];
+          "clap_derive" = [ "dep:clap_derive" ];
+          "color" = [ "atty" "termcolor" ];
+          "debug" = [ "clap_derive/debug" "backtrace" ];
+          "default" = [ "std" "color" "suggestions" ];
+          "deprecated" = [ "clap_derive/deprecated" ];
+          "derive" = [ "clap_derive" "once_cell" ];
+          "once_cell" = [ "dep:once_cell" ];
+          "regex" = [ "dep:regex" ];
+          "std" = [ "indexmap/std" ];
+          "strsim" = [ "dep:strsim" ];
+          "suggestions" = [ "strsim" ];
+          "termcolor" = [ "dep:termcolor" ];
+          "terminal_size" = [ "dep:terminal_size" ];
+          "unicase" = [ "dep:unicase" ];
+          "unicode" = [ "textwrap/unicode-width" "unicase" ];
+          "unstable-doc" = [ "derive" "cargo" "wrap_help" "yaml" "env" "unicode" "regex" "unstable-replace" "unstable-grouped" ];
+          "unstable-v4" = [ "clap_derive/unstable-v4" "deprecated" ];
+          "wrap_help" = [ "terminal_size" "textwrap/terminal_size" ];
+          "yaml" = [ "yaml-rust" ];
+          "yaml-rust" = [ "dep:yaml-rust" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "clap 4.2.7" = rec {
+        crateName = "clap";
+        version = "4.2.7";
+        edition = "2021";
+        crateBin = [ ];
+        sha256 = "0f69iwzh30wbf01ka1k4fa1mxzh22b4iwqs3i6bd49dly6dizlil";
+        dependencies = [
+          {
+            name = "clap_builder";
+            packageId = "clap_builder";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "clap_derive";
+            packageId = "clap_derive";
+            optional = true;
+          }
+          {
+            name = "once_cell";
+            packageId = "once_cell";
+            optional = true;
+          }
+        ];
+        features = {
+          "cargo" = [ "clap_builder/cargo" ];
+          "color" = [ "clap_builder/color" ];
+          "debug" = [ "clap_builder/debug" "clap_derive?/debug" ];
+          "default" = [ "std" "color" "help" "usage" "error-context" "suggestions" ];
+          "deprecated" = [ "clap_builder/deprecated" "clap_derive?/deprecated" ];
+          "derive" = [ "dep:clap_derive" "dep:once_cell" ];
+          "env" = [ "clap_builder/env" ];
+          "error-context" = [ "clap_builder/error-context" ];
+          "help" = [ "clap_builder/help" ];
+          "std" = [ "clap_builder/std" ];
+          "string" = [ "clap_builder/string" ];
+          "suggestions" = [ "clap_builder/suggestions" ];
+          "unicode" = [ "clap_builder/unicode" ];
+          "unstable-doc" = [ "clap_builder/unstable-doc" "derive" ];
+          "unstable-styles" = [ "clap_builder/unstable-styles" ];
+          "unstable-v5" = [ "clap_builder/unstable-v5" "clap_derive?/unstable-v5" "deprecated" ];
+          "usage" = [ "clap_builder/usage" ];
+          "wrap_help" = [ "clap_builder/wrap_help" ];
+        };
+        resolvedDefaultFeatures = [ "color" "default" "derive" "env" "error-context" "help" "std" "suggestions" "usage" ];
+      };
+      "clap_builder" = rec {
+        crateName = "clap_builder";
+        version = "4.2.7";
+        edition = "2021";
+        sha256 = "1gbhk6r14gr0yxc8qs4flbvn5j1302ikk522ys7263snzdwqqk4i";
+        dependencies = [
+          {
+            name = "anstream";
+            packageId = "anstream";
+            optional = true;
+          }
+          {
+            name = "anstyle";
+            packageId = "anstyle";
+          }
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "clap_lex";
+            packageId = "clap_lex 0.4.1";
+          }
+          {
+            name = "strsim";
+            packageId = "strsim";
+            optional = true;
+          }
+        ];
+        features = {
+          "cargo" = [ "dep:once_cell" ];
+          "color" = [ "dep:anstream" ];
+          "debug" = [ "dep:backtrace" ];
+          "default" = [ "std" "color" "help" "usage" "error-context" "suggestions" ];
+          "std" = [ "anstyle/std" ];
+          "suggestions" = [ "dep:strsim" "error-context" ];
+          "unicode" = [ "dep:unicode-width" "dep:unicase" ];
+          "unstable-doc" = [ "cargo" "wrap_help" "env" "unicode" "string" "unstable-styles" ];
+          "unstable-styles" = [ "color" ];
+          "unstable-v5" = [ "deprecated" ];
+          "wrap_help" = [ "help" "dep:terminal_size" ];
+        };
+        resolvedDefaultFeatures = [ "color" "env" "error-context" "help" "std" "suggestions" "usage" ];
+      };
+      "clap_derive" = rec {
+        crateName = "clap_derive";
+        version = "4.2.0";
+        edition = "2021";
+        sha256 = "1i65yn9n1hydvwrimqp9civ67h1iwd9v1y4yi6z7vf6nav6l95iz";
+        procMacro = true;
+        dependencies = [
+          {
+            name = "heck";
+            packageId = "heck";
+          }
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+            features = [ "full" ];
+          }
+        ];
+        features = {
+          "raw-deprecated" = [ "deprecated" ];
+          "unstable-v5" = [ "deprecated" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "clap_lex 0.2.4" = rec {
+        crateName = "clap_lex";
+        version = "0.2.4";
+        edition = "2021";
+        sha256 = "1ib1a9v55ybnaws11l63az0jgz5xiy24jkdgsmyl7grcm3sz4l18";
+        dependencies = [
+          {
+            name = "os_str_bytes";
+            packageId = "os_str_bytes";
+            usesDefaultFeatures = false;
+            features = [ "raw_os_str" ];
+          }
+        ];
+
+      };
+      "clap_lex 0.4.1" = rec {
+        crateName = "clap_lex";
+        version = "0.4.1";
+        edition = "2021";
+        sha256 = "18dyxyc0g5xrazj8k6mdjd8v3fvka8z3b9k8yl13avlczskdabca";
+
+      };
+      "clipboard-win" = rec {
+        crateName = "clipboard-win";
+        version = "4.5.0";
+        edition = "2018";
+        sha256 = "0qh3rypkf1lazniq4nr04hxsck0d55rigb5sjvpvgnap4dyc54bi";
+        authors = [
+          "Douman <douman@gmx.se>"
+        ];
+        dependencies = [
+          {
+            name = "error-code";
+            packageId = "error-code";
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "str-buf";
+            packageId = "str-buf";
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            usesDefaultFeatures = false;
+            target = { target, features }: (target."windows" or false);
+            features = [ "basetsd" "shellapi" "winbase" "winuser" "winerror" "stringapiset" "errhandlingapi" "synchapi" ];
+          }
+        ];
+        features = {
+          "std" = [ "error-code/std" ];
+        };
+      };
+      "codemap" = rec {
+        crateName = "codemap";
+        version = "0.1.3";
+        edition = "2015";
+        sha256 = "091azkslwkcijj3lp9ymb084y9a0wm4fkil7m613ja68r2snkrxr";
+        authors = [
+          "Kevin Mehall <km@kevinmehall.net>"
+        ];
+
+      };
+      "codemap-diagnostic" = rec {
+        crateName = "codemap-diagnostic";
+        version = "0.1.1";
+        edition = "2015";
+        sha256 = "0a2hpb57f97816fjz89qrsz1b5r4j2s2a1p9z58ffx17iszfd82b";
+        authors = [
+          "Kevin Mehall <km@kevinmehall.net>"
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "atty";
+            packageId = "atty";
+          }
+          {
+            name = "codemap";
+            packageId = "codemap";
+          }
+          {
+            name = "termcolor";
+            packageId = "termcolor";
+          }
+        ];
+
+      };
+      "colorchoice" = rec {
+        crateName = "colorchoice";
+        version = "1.0.0";
+        edition = "2021";
+        sha256 = "1ix7w85kwvyybwi2jdkl3yva2r2bvdcc3ka2grjfzfgrapqimgxc";
+
+      };
+      "constant_time_eq" = rec {
+        crateName = "constant_time_eq";
+        version = "0.2.5";
+        edition = "2021";
+        sha256 = "0sy7bs12dfa2d5hw7759b0mvjqcs85giajg4qyg39xq8a1s8wh8k";
+        authors = [
+          "Cesar Eduardo Barros <cesarb@cesarb.eti.br>"
+        ];
+
+      };
+      "count-write" = rec {
+        crateName = "count-write";
+        version = "0.1.0";
+        edition = "2018";
+        sha256 = "11bswmgr81s3jagdci1pr6qh9vnz9zsbbf2dqpi260daa2mhgmff";
+        authors = [
+          "SOFe <sofe2038@gmail.com>"
+        ];
+        features = {
+          "futures" = [ "futures-io-preview" ];
+          "futures-io-preview" = [ "dep:futures-io-preview" ];
+          "tokio" = [ "tokio-io" ];
+          "tokio-io" = [ "dep:tokio-io" ];
+        };
+      };
+      "countme" = rec {
+        crateName = "countme";
+        version = "3.0.1";
+        edition = "2018";
+        sha256 = "0dn62hhvgmwyxslh14r4nlbvz8h50cp5mnn1qhqsw63vs7yva13p";
+        authors = [
+          "Aleksey Kladov <aleksey.kladov@gmail.com>"
+        ];
+        features = {
+          "dashmap" = [ "dep:dashmap" ];
+          "enable" = [ "dashmap" "once_cell" "rustc-hash" ];
+          "once_cell" = [ "dep:once_cell" ];
+          "print_at_exit" = [ "enable" ];
+          "rustc-hash" = [ "dep:rustc-hash" ];
+        };
+      };
+      "cpufeatures" = rec {
+        crateName = "cpufeatures";
+        version = "0.2.7";
+        edition = "2018";
+        sha256 = "0n7y7ls0g1svrjr6ymjv338q8ajc91sv2amdpgn7pi0j42m1wk1y";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "aarch64-linux-android");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (("aarch64" == target."arch") && ("linux" == target."os"));
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (("aarch64" == target."arch") && ("apple" == target."vendor"));
+          }
+        ];
+
+      };
+      "crc32fast" = rec {
+        crateName = "crc32fast";
+        version = "1.3.2";
+        edition = "2015";
+        sha256 = "03c8f29yx293yf43xar946xbls1g60c207m9drf8ilqhr25vsh5m";
+        authors = [
+          "Sam Rijs <srijs@airpost.net>"
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "criterion" = rec {
+        crateName = "criterion";
+        version = "0.4.0";
+        edition = "2018";
+        sha256 = "1jsl4r0yc3fpkyjbi8aa1jrm69apqq9rxwnjnd9brqmaq44nxiz7";
+        authors = [
+          "Jorge Aparicio <japaricious@gmail.com>"
+          "Brook Heisler <brookheisler@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "anes";
+            packageId = "anes";
+          }
+          {
+            name = "atty";
+            packageId = "atty";
+          }
+          {
+            name = "cast";
+            packageId = "cast";
+          }
+          {
+            name = "ciborium";
+            packageId = "ciborium";
+          }
+          {
+            name = "clap";
+            packageId = "clap 3.2.25";
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+          {
+            name = "criterion-plot";
+            packageId = "criterion-plot";
+          }
+          {
+            name = "itertools";
+            packageId = "itertools";
+          }
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+          }
+          {
+            name = "num-traits";
+            packageId = "num-traits";
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+          {
+            name = "oorandom";
+            packageId = "oorandom";
+          }
+          {
+            name = "plotters";
+            packageId = "plotters";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "svg_backend" "area_series" "line_series" ];
+          }
+          {
+            name = "rayon";
+            packageId = "rayon";
+            optional = true;
+          }
+          {
+            name = "regex";
+            packageId = "regex";
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+          }
+          {
+            name = "serde_derive";
+            packageId = "serde_derive";
+          }
+          {
+            name = "serde_json";
+            packageId = "serde_json";
+          }
+          {
+            name = "tinytemplate";
+            packageId = "tinytemplate";
+          }
+          {
+            name = "walkdir";
+            packageId = "walkdir";
+          }
+        ];
+        features = {
+          "async" = [ "futures" ];
+          "async-std" = [ "dep:async-std" ];
+          "async_futures" = [ "futures/executor" "async" ];
+          "async_smol" = [ "smol" "async" ];
+          "async_std" = [ "async-std" "async" ];
+          "async_tokio" = [ "tokio" "async" ];
+          "csv" = [ "dep:csv" ];
+          "csv_output" = [ "csv" ];
+          "default" = [ "rayon" "plotters" "cargo_bench_support" ];
+          "futures" = [ "dep:futures" ];
+          "plotters" = [ "dep:plotters" ];
+          "rayon" = [ "dep:rayon" ];
+          "smol" = [ "dep:smol" ];
+          "stable" = [ "csv_output" "html_reports" "async_futures" "async_smol" "async_tokio" "async_std" ];
+          "tokio" = [ "dep:tokio" ];
+        };
+        resolvedDefaultFeatures = [ "cargo_bench_support" "default" "plotters" "rayon" ];
+      };
+      "criterion-plot" = rec {
+        crateName = "criterion-plot";
+        version = "0.5.0";
+        edition = "2018";
+        sha256 = "1c866xkjqqhzg4cjvg01f8w6xc1j3j7s58rdksl52skq89iq4l3b";
+        authors = [
+          "Jorge Aparicio <japaricious@gmail.com>"
+          "Brook Heisler <brookheisler@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "cast";
+            packageId = "cast";
+          }
+          {
+            name = "itertools";
+            packageId = "itertools";
+          }
+        ];
+
+      };
+      "crossbeam-channel" = rec {
+        crateName = "crossbeam-channel";
+        version = "0.5.8";
+        edition = "2018";
+        sha256 = "004jz4wxp9k26z657i7rsh9s7586dklx2c5aqf1n3w1dgzvjng53";
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "crossbeam-utils";
+            packageId = "crossbeam-utils";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "crossbeam-utils" = [ "dep:crossbeam-utils" ];
+          "default" = [ "std" ];
+          "std" = [ "crossbeam-utils/std" ];
+        };
+        resolvedDefaultFeatures = [ "crossbeam-utils" "default" "std" ];
+      };
+      "crossbeam-deque" = rec {
+        crateName = "crossbeam-deque";
+        version = "0.8.3";
+        edition = "2018";
+        sha256 = "1vqczbcild7nczh5z116w8w46z991kpjyw7qxkf24c14apwdcvyf";
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "crossbeam-epoch";
+            packageId = "crossbeam-epoch";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "crossbeam-utils";
+            packageId = "crossbeam-utils";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "crossbeam-epoch" = [ "dep:crossbeam-epoch" ];
+          "crossbeam-utils" = [ "dep:crossbeam-utils" ];
+          "default" = [ "std" ];
+          "std" = [ "crossbeam-epoch/std" "crossbeam-utils/std" ];
+        };
+        resolvedDefaultFeatures = [ "crossbeam-epoch" "crossbeam-utils" "default" "std" ];
+      };
+      "crossbeam-epoch" = rec {
+        crateName = "crossbeam-epoch";
+        version = "0.9.14";
+        edition = "2018";
+        sha256 = "15anryfq33mhxnlw95ajixnzznxays3gpvaas6lraci7hlzmzga6";
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "crossbeam-utils";
+            packageId = "crossbeam-utils";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "memoffset";
+            packageId = "memoffset";
+          }
+          {
+            name = "scopeguard";
+            packageId = "scopeguard";
+            usesDefaultFeatures = false;
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "loom" = [ "loom-crate" "crossbeam-utils/loom" ];
+          "loom-crate" = [ "dep:loom-crate" ];
+          "nightly" = [ "crossbeam-utils/nightly" ];
+          "std" = [ "alloc" "crossbeam-utils/std" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "std" ];
+      };
+      "crossbeam-utils" = rec {
+        crateName = "crossbeam-utils";
+        version = "0.8.15";
+        edition = "2018";
+        sha256 = "0jwq8srmjcwvq9q883k9zyb26qqznaj4jjqdxmvw7xcmrkc3q1iw";
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "loom" = [ "dep:loom" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "crypto-common" = rec {
+        crateName = "crypto-common";
+        version = "0.1.6";
+        edition = "2018";
+        sha256 = "1cvby95a6xg7kxdz5ln3rl9xh66nz66w46mm3g56ri1z5x815yqv";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "generic-array";
+            packageId = "generic-array 0.14.7";
+            features = [ "more_lengths" ];
+          }
+          {
+            name = "typenum";
+            packageId = "typenum";
+          }
+        ];
+        features = {
+          "getrandom" = [ "rand_core/getrandom" ];
+          "rand_core" = [ "dep:rand_core" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "ctor" = rec {
+        crateName = "ctor";
+        version = "0.1.26";
+        edition = "2018";
+        sha256 = "15m0wqhv12p25xkxz5dxvg23r7a6bkh7p8zi1cdhgswjhdl028vd";
+        procMacro = true;
+        authors = [
+          "Matt Mastracci <matthew@mastracci.com>"
+        ];
+        dependencies = [
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            usesDefaultFeatures = false;
+            features = [ "full" "parsing" "printing" "proc-macro" ];
+          }
+        ];
+
+      };
+      "data-encoding" = rec {
+        crateName = "data-encoding";
+        version = "2.3.3";
+        edition = "2018";
+        sha256 = "1yq8jnivxsjzl3mjbjdjg5kfvd17wawbmg1jvsfw6cqmn1n6dn13";
+        authors = [
+          "Julien Cretin <git@ia0.eu>"
+        ];
+        features = {
+          "default" = [ "std" ];
+          "std" = [ "alloc" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "std" ];
+      };
+      "diff" = rec {
+        crateName = "diff";
+        version = "0.1.13";
+        edition = "2015";
+        sha256 = "1j0nzjxci2zqx63hdcihkp0a4dkdmzxd7my4m7zk6cjyfy34j9an";
+        authors = [
+          "Utkarsh Kukreti <utkarshkukreti@gmail.com>"
+        ];
+
+      };
+      "digest 0.10.6" = rec {
+        crateName = "digest";
+        version = "0.10.6";
+        edition = "2018";
+        sha256 = "0vz74785s96g727vg37iwkjvbkcfzp093j49ihhyf8sh9s7kfs41";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "block-buffer";
+            packageId = "block-buffer 0.10.4";
+            optional = true;
+          }
+          {
+            name = "crypto-common";
+            packageId = "crypto-common";
+          }
+          {
+            name = "subtle";
+            packageId = "subtle";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "blobby" = [ "dep:blobby" ];
+          "block-buffer" = [ "dep:block-buffer" ];
+          "const-oid" = [ "dep:const-oid" ];
+          "core-api" = [ "block-buffer" ];
+          "default" = [ "core-api" ];
+          "dev" = [ "blobby" ];
+          "mac" = [ "subtle" ];
+          "oid" = [ "const-oid" ];
+          "rand_core" = [ "crypto-common/rand_core" ];
+          "std" = [ "alloc" "crypto-common/std" ];
+          "subtle" = [ "dep:subtle" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "block-buffer" "core-api" "default" "mac" "std" "subtle" ];
+      };
+      "digest 0.8.1" = rec {
+        crateName = "digest";
+        version = "0.8.1";
+        edition = "2015";
+        sha256 = "1madjl27f3kj5ql7kwgvb9c8b7yb7bv7yfgx7rqzj4i3fp4cil7k";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "generic-array";
+            packageId = "generic-array 0.12.4";
+          }
+        ];
+        features = {
+          "blobby" = [ "dep:blobby" ];
+          "dev" = [ "blobby" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "dirs" = rec {
+        crateName = "dirs";
+        version = "4.0.0";
+        edition = "2015";
+        sha256 = "0n8020zl4f0frfnzvgb9agvk4a14i1kjz4daqnxkgslndwmaffna";
+        authors = [
+          "Simon Ochsenreither <simon@ochsenreither.de>"
+        ];
+        dependencies = [
+          {
+            name = "dirs-sys";
+            packageId = "dirs-sys";
+          }
+        ];
+
+      };
+      "dirs-next" = rec {
+        crateName = "dirs-next";
+        version = "2.0.0";
+        edition = "2018";
+        sha256 = "1q9kr151h9681wwp6is18750ssghz6j9j7qm7qi1ngcwy7mzi35r";
+        authors = [
+          "The @xdg-rs members"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "dirs-sys-next";
+            packageId = "dirs-sys-next";
+          }
+        ];
+
+      };
+      "dirs-sys" = rec {
+        crateName = "dirs-sys";
+        version = "0.3.7";
+        edition = "2015";
+        sha256 = "19md1cnkazham8a6kh22v12d8hh3raqahfk6yb043vrjr68is78v";
+        authors = [
+          "Simon Ochsenreither <simon@ochsenreither.de>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "redox_users";
+            packageId = "redox_users";
+            usesDefaultFeatures = false;
+            target = { target, features }: ("redox" == target."os");
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "knownfolders" "objbase" "shlobj" "winbase" "winerror" ];
+          }
+        ];
+
+      };
+      "dirs-sys-next" = rec {
+        crateName = "dirs-sys-next";
+        version = "0.1.2";
+        edition = "2018";
+        sha256 = "0kavhavdxv4phzj4l0psvh55hszwnr0rcz8sxbvx20pyqi2a3gaf";
+        authors = [
+          "The @xdg-rs members"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "redox_users";
+            packageId = "redox_users";
+            usesDefaultFeatures = false;
+            target = { target, features }: ("redox" == target."os");
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "knownfolders" "objbase" "shlobj" "winbase" "winerror" ];
+          }
+        ];
+
+      };
+      "either" = rec {
+        crateName = "either";
+        version = "1.8.1";
+        edition = "2018";
+        sha256 = "14bdy4qsxlfnm4626z4shwaiffi8l5krzkn7ykki1jgqzsrapjkz";
+        authors = [
+          "bluss"
+        ];
+        features = {
+          "default" = [ "use_std" ];
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "default" "use_std" ];
+      };
+      "endian-type" = rec {
+        crateName = "endian-type";
+        version = "0.1.2";
+        edition = "2015";
+        sha256 = "0bbh88zaig1jfqrm7w3gx0pz81kw2jakk3055vbgapw3dmk08ky3";
+        authors = [
+          "Lolirofle <lolipopple@hotmail.com>"
+        ];
+
+      };
+      "errno" = rec {
+        crateName = "errno";
+        version = "0.3.1";
+        edition = "2018";
+        sha256 = "0fp7qy6fwagrnmi45msqnl01vksqwdb2qbbv60n9cz7rf0xfrksb";
+        authors = [
+          "Chris Wong <lambda.fairy@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "errno-dragonfly";
+            packageId = "errno-dragonfly";
+            target = { target, features }: ("dragonfly" == target."os");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: ("hermit" == target."os");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: ("wasi" == target."os");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_Foundation" "Win32_System_Diagnostics_Debug" ];
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+      };
+      "errno-dragonfly" = rec {
+        crateName = "errno-dragonfly";
+        version = "0.1.2";
+        edition = "2018";
+        sha256 = "1grrmcm6q8512hkq5yzch3yv8wafflc2apbmsaabiyk44yqz2s5a";
+        authors = [
+          "Michael Neumann <mneumann@ntecs.de>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            usesDefaultFeatures = false;
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "cc";
+            packageId = "cc";
+          }
+        ];
+
+      };
+      "error-code" = rec {
+        crateName = "error-code";
+        version = "2.3.1";
+        edition = "2018";
+        sha256 = "08baxlf8qz01lgjsdbfhs193r9y1nlc566s5xvzyf4dzwy8qkwb4";
+        authors = [
+          "Douman <douman@gmx.se>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "str-buf";
+            packageId = "str-buf";
+          }
+        ];
+        features = { };
+      };
+      "fake-simd" = rec {
+        crateName = "fake-simd";
+        version = "0.1.2";
+        edition = "2015";
+        sha256 = "1vfylvk4va2ivqx85603lyqqp0zk52cgbs4n5nfbbbqx577qm2p8";
+        authors = [
+          "The Rust-Crypto Project Developers"
+        ];
+
+      };
+      "fastrand" = rec {
+        crateName = "fastrand";
+        version = "1.9.0";
+        edition = "2018";
+        sha256 = "1gh12m56265ihdbzh46bhh0jf74i197wm51jg1cw75q7ggi96475";
+        authors = [
+          "Stjepan Glavina <stjepang@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "instant";
+            packageId = "instant";
+            target = { target, features }: (("wasm32" == target."arch") && (!("wasi" == target."os")));
+          }
+        ];
+        devDependencies = [
+          {
+            name = "instant";
+            packageId = "instant";
+            target = { target, features }: (("wasm32" == target."arch") && (!("wasi" == target."os")));
+            features = [ "wasm-bindgen" ];
+          }
+        ];
+
+      };
+      "fd-lock" = rec {
+        crateName = "fd-lock";
+        version = "3.0.12";
+        edition = "2018";
+        sha256 = "0hlnn1302p37qlc9xl2k5y0vw8q8id5kg59an6riy89hjlynpbir";
+        authors = [
+          "Yoshua Wuyts <yoshuawuyts@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "rustix";
+            packageId = "rustix";
+            target = { target, features }: (target."unix" or false);
+            features = [ "fs" ];
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_Foundation" "Win32_Storage_FileSystem" "Win32_System_IO" ];
+          }
+        ];
+
+      };
+      "fixedbitset" = rec {
+        crateName = "fixedbitset";
+        version = "0.4.2";
+        edition = "2015";
+        sha256 = "101v41amgv5n9h4hcghvrbfk5vrncx1jwm35rn5szv4rk55i7rqc";
+        authors = [
+          "bluss"
+        ];
+        features = {
+          "default" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "fnv" = rec {
+        crateName = "fnv";
+        version = "1.0.7";
+        edition = "2015";
+        sha256 = "1hc2mcqha06aibcaza94vbi81j6pr9a1bbxrxjfhc91zin8yr7iz";
+        libPath = "lib.rs";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "fs2" = rec {
+        crateName = "fs2";
+        version = "0.4.3";
+        edition = "2015";
+        sha256 = "04v2hwk7035c088f19mfl5b1lz84gnvv2hv6m935n0hmirszqr4m";
+        authors = [
+          "Dan Burkert <dan@danburkert.com>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "handleapi" "processthreadsapi" "winerror" "fileapi" "winbase" "std" ];
+          }
+        ];
+
+      };
+      "fuchsia-cprng" = rec {
+        crateName = "fuchsia-cprng";
+        version = "0.1.1";
+        edition = "2018";
+        sha256 = "1fnkqrbz7ixxzsb04bsz9p0zzazanma8znfdqjvh39n14vapfvx0";
+        authors = [
+          "Erick Tryzelaar <etryzelaar@google.com>"
+        ];
+
+      };
+      "futures" = rec {
+        crateName = "futures";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "0h7c1xvxk751c7xlnph6fh3rb77z4lig4qif7f8q79db2az2ld13";
+        dependencies = [
+          {
+            name = "futures-channel";
+            packageId = "futures-channel";
+            usesDefaultFeatures = false;
+            features = [ "sink" ];
+          }
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-executor";
+            packageId = "futures-executor";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-io";
+            packageId = "futures-io";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-sink";
+            packageId = "futures-sink";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-task";
+            packageId = "futures-task";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+            features = [ "sink" ];
+          }
+        ];
+        features = {
+          "alloc" = [ "futures-core/alloc" "futures-task/alloc" "futures-sink/alloc" "futures-channel/alloc" "futures-util/alloc" ];
+          "async-await" = [ "futures-util/async-await" "futures-util/async-await-macro" ];
+          "bilock" = [ "futures-util/bilock" ];
+          "compat" = [ "std" "futures-util/compat" ];
+          "default" = [ "std" "async-await" "executor" ];
+          "executor" = [ "std" "futures-executor/std" ];
+          "futures-executor" = [ "dep:futures-executor" ];
+          "io-compat" = [ "compat" "futures-util/io-compat" ];
+          "std" = [ "alloc" "futures-core/std" "futures-task/std" "futures-io/std" "futures-sink/std" "futures-util/std" "futures-util/io" "futures-util/channel" ];
+          "thread-pool" = [ "executor" "futures-executor/thread-pool" ];
+          "unstable" = [ "futures-core/unstable" "futures-task/unstable" "futures-channel/unstable" "futures-io/unstable" "futures-util/unstable" ];
+          "write-all-vectored" = [ "futures-util/write-all-vectored" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "async-await" "default" "executor" "futures-executor" "std" ];
+      };
+      "futures-channel" = rec {
+        crateName = "futures-channel";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "1wmm9wm5zjigxz61qkscmxp7c30zp08dy63spjz5pch9gva1hmcm";
+        dependencies = [
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-sink";
+            packageId = "futures-sink";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "alloc" = [ "futures-core/alloc" ];
+          "default" = [ "std" ];
+          "futures-sink" = [ "dep:futures-sink" ];
+          "sink" = [ "futures-sink" ];
+          "std" = [ "alloc" "futures-core/std" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "futures-sink" "sink" "std" ];
+      };
+      "futures-core" = rec {
+        crateName = "futures-core";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "137fdxy5amg9zkpa1kqnj7bnha6b94fmddz59w973x96gqxmijjb";
+        features = {
+          "default" = [ "std" ];
+          "portable-atomic" = [ "dep:portable-atomic" ];
+          "std" = [ "alloc" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "std" ];
+      };
+      "futures-executor" = rec {
+        crateName = "futures-executor";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "1q468di96knnla72xdvswic1ir2qkrf5czsdigc5n4l86a1fxv6c";
+        dependencies = [
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-task";
+            packageId = "futures-task";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "num_cpus" = [ "dep:num_cpus" ];
+          "std" = [ "futures-core/std" "futures-task/std" "futures-util/std" ];
+          "thread-pool" = [ "std" "num_cpus" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "futures-io" = rec {
+        crateName = "futures-io";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "0r4rhkdhq1my4fahlhz59barqa511bylq813w3w4gvbidq4p9zsg";
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "futures-macro" = rec {
+        crateName = "futures-macro";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "0wpfsqxwqk5k569xl0jzz4zxy85x695mndf7y9jn66q6jid59jl9";
+        procMacro = true;
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+            features = [ "full" ];
+          }
+        ];
+
+      };
+      "futures-sink" = rec {
+        crateName = "futures-sink";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "0vkv4frf4c6gm1ag9imjz8d0xvpnn22lkylsls0rffx147zf8fzl";
+        features = {
+          "default" = [ "std" ];
+          "std" = [ "alloc" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "std" ];
+      };
+      "futures-task" = rec {
+        crateName = "futures-task";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "0ravgihyarbplj32zp60asirfnaalw2wfsa0afhnl3kcpqrd3lvn";
+        features = {
+          "default" = [ "std" ];
+          "std" = [ "alloc" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "std" ];
+      };
+      "futures-util" = rec {
+        crateName = "futures-util";
+        version = "0.3.28";
+        edition = "2018";
+        sha256 = "0cwmls9369w6q6hwlbm10q0plr6hmg8w28fpqvv4rmbjnx01xc16";
+        dependencies = [
+          {
+            name = "futures-channel";
+            packageId = "futures-channel";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-io";
+            packageId = "futures-io";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+          {
+            name = "futures-macro";
+            packageId = "futures-macro";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-sink";
+            packageId = "futures-sink";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-task";
+            packageId = "futures-task";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "memchr";
+            packageId = "memchr";
+            optional = true;
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "pin-utils";
+            packageId = "pin-utils";
+          }
+          {
+            name = "slab";
+            packageId = "slab";
+            optional = true;
+          }
+        ];
+        features = {
+          "alloc" = [ "futures-core/alloc" "futures-task/alloc" ];
+          "async-await-macro" = [ "async-await" "futures-macro" ];
+          "channel" = [ "std" "futures-channel" ];
+          "compat" = [ "std" "futures_01" ];
+          "default" = [ "std" "async-await" "async-await-macro" ];
+          "futures-channel" = [ "dep:futures-channel" ];
+          "futures-io" = [ "dep:futures-io" ];
+          "futures-macro" = [ "dep:futures-macro" ];
+          "futures-sink" = [ "dep:futures-sink" ];
+          "futures_01" = [ "dep:futures_01" ];
+          "io" = [ "std" "futures-io" "memchr" ];
+          "io-compat" = [ "io" "compat" "tokio-io" ];
+          "memchr" = [ "dep:memchr" ];
+          "portable-atomic" = [ "futures-core/portable-atomic" ];
+          "sink" = [ "futures-sink" ];
+          "slab" = [ "dep:slab" ];
+          "std" = [ "alloc" "futures-core/std" "futures-task/std" "slab" ];
+          "tokio-io" = [ "dep:tokio-io" ];
+          "unstable" = [ "futures-core/unstable" "futures-task/unstable" ];
+          "write-all-vectored" = [ "io" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "async-await" "async-await-macro" "channel" "futures-channel" "futures-io" "futures-macro" "futures-sink" "io" "memchr" "sink" "slab" "std" ];
+      };
+      "fxhash" = rec {
+        crateName = "fxhash";
+        version = "0.2.1";
+        edition = "2015";
+        sha256 = "037mb9ichariqi45xm6mz0b11pa92gj38ba0409z3iz239sns6y3";
+        libPath = "lib.rs";
+        authors = [
+          "cbreeden <github@u.breeden.cc>"
+        ];
+        dependencies = [
+          {
+            name = "byteorder";
+            packageId = "byteorder";
+          }
+        ];
+
+      };
+      "genawaiter" = rec {
+        crateName = "genawaiter";
+        version = "0.99.1";
+        edition = "2018";
+        sha256 = "1861a6vy9lc9a8lbw496m9j9jcjcn9nf7rkm6jqkkpnb3cvd0sy8";
+        authors = [
+          "John Simon <john@whatisaph.one>"
+        ];
+        dependencies = [
+          {
+            name = "genawaiter-macro";
+            packageId = "genawaiter-macro";
+          }
+        ];
+        features = {
+          "default" = [ "proc_macro" ];
+          "futures-core" = [ "dep:futures-core" ];
+          "futures03" = [ "futures-core" ];
+          "genawaiter-proc-macro" = [ "dep:genawaiter-proc-macro" ];
+          "proc-macro-hack" = [ "dep:proc-macro-hack" ];
+          "proc_macro" = [ "genawaiter-proc-macro" "proc-macro-hack" "genawaiter-macro/proc_macro" ];
+        };
+      };
+      "genawaiter-macro" = rec {
+        crateName = "genawaiter-macro";
+        version = "0.99.1";
+        edition = "2018";
+        sha256 = "1g6zmr88fk48f1ksz9ik1i2mwjsiam9s4p9aybhvs2zwzphxychb";
+        authors = [
+          "Devin R <devin.ragotzy@gmail.com>"
+        ];
+        features = { };
+      };
+      "generic-array 0.12.4" = rec {
+        crateName = "generic-array";
+        version = "0.12.4";
+        edition = "2015";
+        sha256 = "1gfpay78vijl9vrwl1k9v7fbvbhkhcmnrk4kfg9l6x24y4s9zpzz";
+        libName = "generic_array";
+        authors = [
+          "Bartłomiej Kamiński <fizyk20@gmail.com>"
+          "Aaron Trent <novacrazy@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "typenum";
+            packageId = "typenum";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "generic-array 0.14.7" = rec {
+        crateName = "generic-array";
+        version = "0.14.7";
+        edition = "2015";
+        sha256 = "16lyyrzrljfq424c3n8kfwkqihlimmsg5nhshbbp48np3yjrqr45";
+        libName = "generic_array";
+        authors = [
+          "Bartłomiej Kamiński <fizyk20@gmail.com>"
+          "Aaron Trent <novacrazy@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "typenum";
+            packageId = "typenum";
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "version_check";
+            packageId = "version_check";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+          "zeroize" = [ "dep:zeroize" ];
+        };
+        resolvedDefaultFeatures = [ "more_lengths" ];
+      };
+      "getrandom" = rec {
+        crateName = "getrandom";
+        version = "0.2.9";
+        edition = "2018";
+        sha256 = "1r6p47dd9f9cgiwlxmksammbfwnhsv5hjkhd0kjsgnzanad1spn8";
+        authors = [
+          "The Rand Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            usesDefaultFeatures = false;
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "wasi";
+            packageId = "wasi";
+            usesDefaultFeatures = false;
+            target = { target, features }: ("wasi" == target."os");
+          }
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "js" = [ "wasm-bindgen" "js-sys" ];
+          "js-sys" = [ "dep:js-sys" ];
+          "rustc-dep-of-std" = [ "compiler_builtins" "core" "libc/rustc-dep-of-std" "wasi/rustc-dep-of-std" ];
+          "wasm-bindgen" = [ "dep:wasm-bindgen" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "glob" = rec {
+        crateName = "glob";
+        version = "0.3.1";
+        edition = "2015";
+        sha256 = "16zca52nglanv23q5qrwd5jinw3d3as5ylya6y1pbx47vkxvrynj";
+        authors = [
+          "The Rust Project Developers"
+        ];
+
+      };
+      "h2" = rec {
+        crateName = "h2";
+        version = "0.3.18";
+        edition = "2018";
+        sha256 = "08ffidcaswjn30c63whc17s93nr1afh8l4xmd21nhywqq8aaky0p";
+        authors = [
+          "Carl Lerche <me@carllerche.com>"
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "fnv";
+            packageId = "fnv";
+          }
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-sink";
+            packageId = "futures-sink";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "indexmap";
+            packageId = "indexmap";
+            features = [ "std" ];
+          }
+          {
+            name = "slab";
+            packageId = "slab";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "io-util" ];
+          }
+          {
+            name = "tokio-util";
+            packageId = "tokio-util";
+            features = [ "codec" ];
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "rt-multi-thread" "macros" "sync" "net" ];
+          }
+        ];
+        features = { };
+      };
+      "half" = rec {
+        crateName = "half";
+        version = "1.8.2";
+        edition = "2018";
+        sha256 = "1mqbmx2m9qd4lslkb42fzgldsklhv9c4bxsc8j82r80d8m24mfza";
+        authors = [
+          "Kathryn Long <squeeself@gmail.com>"
+        ];
+        features = {
+          "bytemuck" = [ "dep:bytemuck" ];
+          "num-traits" = [ "dep:num-traits" ];
+          "serde" = [ "dep:serde" ];
+          "serialize" = [ "serde" ];
+          "std" = [ "alloc" ];
+          "zerocopy" = [ "dep:zerocopy" ];
+        };
+      };
+      "hashbrown" = rec {
+        crateName = "hashbrown";
+        version = "0.12.3";
+        edition = "2021";
+        sha256 = "1268ka4750pyg2pbgsr43f0289l5zah4arir2k4igx5a8c6fg7la";
+        authors = [
+          "Amanieu d'Antras <amanieu@gmail.com>"
+        ];
+        features = {
+          "ahash" = [ "dep:ahash" ];
+          "ahash-compile-time-rng" = [ "ahash/compile-time-rng" ];
+          "alloc" = [ "dep:alloc" ];
+          "bumpalo" = [ "dep:bumpalo" ];
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "default" = [ "ahash" "inline-more" ];
+          "rayon" = [ "dep:rayon" ];
+          "rustc-dep-of-std" = [ "nightly" "core" "compiler_builtins" "alloc" "rustc-internal-api" ];
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "inline-more" "raw" ];
+      };
+      "heck" = rec {
+        crateName = "heck";
+        version = "0.4.1";
+        edition = "2018";
+        sha256 = "1a7mqsnycv5z4z5vnv1k34548jzmc0ajic7c1j8jsaspnhw5ql4m";
+        authors = [
+          "Without Boats <woboats@gmail.com>"
+        ];
+        features = {
+          "unicode" = [ "unicode-segmentation" ];
+          "unicode-segmentation" = [ "dep:unicode-segmentation" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "hermit-abi 0.1.19" = rec {
+        crateName = "hermit-abi";
+        version = "0.1.19";
+        edition = "2018";
+        sha256 = "0cxcm8093nf5fyn114w8vxbrbcyvv91d4015rdnlgfll7cs6gd32";
+        authors = [
+          "Stefan Lankes"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "rustc-dep-of-std" = [ "core" "compiler_builtins/rustc-dep-of-std" "libc/rustc-dep-of-std" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "hermit-abi 0.2.6" = rec {
+        crateName = "hermit-abi";
+        version = "0.2.6";
+        edition = "2021";
+        sha256 = "1iz439yz9qzk3rh9pqx2rz5c4107v3qbd7bppfsbzb1mzr02clgf";
+        authors = [
+          "Stefan Lankes"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "alloc" = [ "dep:alloc" ];
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "rustc-dep-of-std" = [ "core" "alloc" "compiler_builtins/rustc-dep-of-std" "libc/rustc-dep-of-std" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "hermit-abi 0.3.1" = rec {
+        crateName = "hermit-abi";
+        version = "0.3.1";
+        edition = "2021";
+        sha256 = "11j2v3q58kmi5mhjvh6hfrb7il2yzg7gmdf5lpwnwwv6qj04im7y";
+        authors = [
+          "Stefan Lankes"
+        ];
+        features = {
+          "alloc" = [ "dep:alloc" ];
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "rustc-dep-of-std" = [ "core" "alloc" "compiler_builtins/rustc-dep-of-std" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "hex" = rec {
+        crateName = "hex";
+        version = "0.3.2";
+        edition = "2015";
+        sha256 = "0xsdcjiik5j750j67zk42qdnmm4ahirk3gmkmcqgq7qls2jjcl40";
+        authors = [
+          "KokaKiwi <kokakiwi@kokakiwi.net>"
+        ];
+        features = { };
+      };
+      "http" = rec {
+        crateName = "http";
+        version = "0.2.9";
+        edition = "2018";
+        sha256 = "10j4jjpngaymxjvi92hllr2y6acr09pq61cvzxd44qzvkb4zyvmx";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+          "Carl Lerche <me@carllerche.com>"
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "fnv";
+            packageId = "fnv";
+          }
+          {
+            name = "itoa";
+            packageId = "itoa";
+          }
+        ];
+
+      };
+      "http-body" = rec {
+        crateName = "http-body";
+        version = "0.4.5";
+        edition = "2018";
+        sha256 = "1l967qwwlvhp198xdrnc0p5d7jwfcp6q2lm510j6zqw4s4b8zwym";
+        authors = [
+          "Carl Lerche <me@carllerche.com>"
+          "Lucio Franco <luciofranco14@gmail.com>"
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+        ];
+
+      };
+      "httparse" = rec {
+        crateName = "httparse";
+        version = "1.8.0";
+        edition = "2018";
+        sha256 = "010rrfahm1jss3p022fqf3j3jmm72vhn4iqhykahb9ynpaag75yq";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "httpdate" = rec {
+        crateName = "httpdate";
+        version = "1.0.2";
+        edition = "2018";
+        sha256 = "08bln7b1ibdw26gl8h4dr6rlybvlkyhlha309xbh9ghxh9nf78f4";
+        authors = [
+          "Pyfisch <pyfisch@posteo.org>"
+        ];
+
+      };
+      "hyper" = rec {
+        crateName = "hyper";
+        version = "0.14.26";
+        edition = "2018";
+        sha256 = "1m27s4p6kv5rbzqkw2lzfs60fwz7wym97zri0j8kn6pilrr2sc5b";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures-channel";
+            packageId = "futures-channel";
+          }
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "h2";
+            packageId = "h2";
+            optional = true;
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "http-body";
+            packageId = "http-body";
+          }
+          {
+            name = "httparse";
+            packageId = "httparse";
+          }
+          {
+            name = "httpdate";
+            packageId = "httpdate";
+          }
+          {
+            name = "itoa";
+            packageId = "itoa";
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "socket2";
+            packageId = "socket2";
+            optional = true;
+            features = [ "all" ];
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "sync" ];
+          }
+          {
+            name = "tower-service";
+            packageId = "tower-service";
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+          {
+            name = "want";
+            packageId = "want";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+            features = [ "alloc" ];
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "fs" "macros" "io-std" "io-util" "rt" "rt-multi-thread" "sync" "time" "test-util" ];
+          }
+        ];
+        features = {
+          "ffi" = [ "libc" ];
+          "full" = [ "client" "http1" "http2" "server" "stream" "runtime" ];
+          "h2" = [ "dep:h2" ];
+          "http2" = [ "h2" ];
+          "libc" = [ "dep:libc" ];
+          "runtime" = [ "tcp" "tokio/rt" "tokio/time" ];
+          "socket2" = [ "dep:socket2" ];
+          "tcp" = [ "socket2" "tokio/net" "tokio/rt" "tokio/time" ];
+        };
+        resolvedDefaultFeatures = [ "client" "default" "full" "h2" "http1" "http2" "runtime" "server" "socket2" "stream" "tcp" ];
+      };
+      "hyper-timeout" = rec {
+        crateName = "hyper-timeout";
+        version = "0.4.1";
+        edition = "2018";
+        sha256 = "1c8k3g8k2yh1gxvsx9p7amkimgxhl9kafwpj7jyf8ywc5r45ifdv";
+        authors = [
+          "Herman J. Radtke III <herman@hermanradtke.com>"
+        ];
+        dependencies = [
+          {
+            name = "hyper";
+            packageId = "hyper";
+            features = [ "client" ];
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+          }
+          {
+            name = "tokio-io-timeout";
+            packageId = "tokio-io-timeout";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "hyper";
+            packageId = "hyper";
+            features = [ "client" "http1" "tcp" ];
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "io-std" "io-util" "macros" ];
+          }
+        ];
+
+      };
+      "imbl" = rec {
+        crateName = "imbl";
+        version = "2.0.0";
+        edition = "2018";
+        sha256 = "03fvbk1g1pqs6j77g76vq5klqi6bx9jl9di782268ilzrmlnp062";
+        authors = [
+          "Bodil Stokke <bodil@bodil.org>"
+          "Joe Neeman <joeneeman@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "bitmaps";
+            packageId = "bitmaps";
+          }
+          {
+            name = "imbl-sized-chunks";
+            packageId = "imbl-sized-chunks";
+          }
+          {
+            name = "proptest";
+            packageId = "proptest";
+            optional = true;
+          }
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.6.4";
+          }
+          {
+            name = "rand_xoshiro";
+            packageId = "rand_xoshiro";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            optional = true;
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "version_check";
+            packageId = "version_check";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "proptest";
+            packageId = "proptest";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+          }
+        ];
+        features = {
+          "arbitrary" = [ "dep:arbitrary" ];
+          "proptest" = [ "dep:proptest" ];
+          "quickcheck" = [ "dep:quickcheck" ];
+          "rayon" = [ "dep:rayon" ];
+          "refpool" = [ "dep:refpool" ];
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "proptest" "serde" ];
+      };
+      "imbl-sized-chunks" = rec {
+        crateName = "imbl-sized-chunks";
+        version = "0.1.1";
+        edition = "2021";
+        sha256 = "0xhhmb7aldl92hxkmsx10n59zxsa0hw4bvykc6jmq72lnah7x5g6";
+        authors = [
+          "Bodil Stokke <bodil@bodil.org>"
+          "Joe Neeman <joeneeman@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "bitmaps";
+            packageId = "bitmaps";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "arbitrary" = [ "dep:arbitrary" ];
+          "array-ops" = [ "dep:array-ops" ];
+          "default" = [ "std" ];
+          "refpool" = [ "dep:refpool" ];
+          "ringbuffer" = [ "array-ops" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "indexmap" = rec {
+        crateName = "indexmap";
+        version = "1.9.3";
+        edition = "2021";
+        sha256 = "16dxmy7yvk51wvnih3a3im6fp5lmx0wx76i03n06wyak6cwhw1xx";
+        dependencies = [
+          {
+            name = "hashbrown";
+            packageId = "hashbrown";
+            usesDefaultFeatures = false;
+            features = [ "raw" ];
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = {
+          "arbitrary" = [ "dep:arbitrary" ];
+          "quickcheck" = [ "dep:quickcheck" ];
+          "rayon" = [ "dep:rayon" ];
+          "rustc-rayon" = [ "dep:rustc-rayon" ];
+          "serde" = [ "dep:serde" ];
+          "serde-1" = [ "serde" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "instant" = rec {
+        crateName = "instant";
+        version = "0.1.12";
+        edition = "2018";
+        sha256 = "0b2bx5qdlwayriidhrag8vhy10kdfimfhmb3jnjmsz2h9j1bwnvs";
+        authors = [
+          "sebcrozet <developer@crozet.re>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+        ];
+        features = {
+          "js-sys" = [ "dep:js-sys" ];
+          "stdweb" = [ "dep:stdweb" ];
+          "wasm-bindgen" = [ "js-sys" "wasm-bindgen_rs" "web-sys" ];
+          "wasm-bindgen_rs" = [ "dep:wasm-bindgen_rs" ];
+          "web-sys" = [ "dep:web-sys" ];
+        };
+      };
+      "io-lifetimes" = rec {
+        crateName = "io-lifetimes";
+        version = "1.0.10";
+        edition = "2018";
+        sha256 = "08625nsz0lgbd7c9lly6b6l45viqpsnj9jbsixd9mrz7596wfrlw";
+        authors = [
+          "Dan Gohman <dev@sunfishcode.online>"
+        ];
+        dependencies = [
+          {
+            name = "hermit-abi";
+            packageId = "hermit-abi 0.3.1";
+            optional = true;
+            target = { target, features }: ("hermit" == target."os");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            optional = true;
+            target = { target, features }: (!(target."windows" or false));
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            optional = true;
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_Foundation" "Win32_Storage_FileSystem" "Win32_Networking_WinSock" "Win32_Security" "Win32_System_IO" "Win32_System_Threading" ];
+          }
+        ];
+        features = {
+          "async-std" = [ "dep:async-std" ];
+          "close" = [ "libc" "hermit-abi" "windows-sys" ];
+          "default" = [ "close" ];
+          "fs-err" = [ "dep:fs-err" ];
+          "hermit-abi" = [ "dep:hermit-abi" ];
+          "libc" = [ "dep:libc" ];
+          "mio" = [ "dep:mio" ];
+          "os_pipe" = [ "dep:os_pipe" ];
+          "socket2" = [ "dep:socket2" ];
+          "tokio" = [ "dep:tokio" ];
+          "windows-sys" = [ "dep:windows-sys" ];
+        };
+        resolvedDefaultFeatures = [ "close" "default" "hermit-abi" "libc" "windows-sys" ];
+      };
+      "is-terminal" = rec {
+        crateName = "is-terminal";
+        version = "0.4.7";
+        edition = "2018";
+        sha256 = "07xyfla3f2jjb666s72la5jvl9zq7mixbqkjvyfi5j018rhr7kxd";
+        authors = [
+          "softprops <d.tangren@gmail.com>"
+          "Dan Gohman <dev@sunfishcode.online>"
+        ];
+        dependencies = [
+          {
+            name = "hermit-abi";
+            packageId = "hermit-abi 0.3.1";
+            target = { target, features }: ("hermit" == target."os");
+          }
+          {
+            name = "io-lifetimes";
+            packageId = "io-lifetimes";
+          }
+          {
+            name = "rustix";
+            packageId = "rustix";
+            target = { target, features }: (!((target."windows" or false) || ("hermit" == target."os") || ("unknown" == target."os")));
+            features = [ "termios" ];
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_Foundation" "Win32_Storage_FileSystem" "Win32_System_Console" ];
+          }
+        ];
+
+      };
+      "itertools" = rec {
+        crateName = "itertools";
+        version = "0.10.5";
+        edition = "2018";
+        sha256 = "0ww45h7nxx5kj6z2y6chlskxd1igvs4j507anr6dzg99x1h25zdh";
+        authors = [
+          "bluss"
+        ];
+        dependencies = [
+          {
+            name = "either";
+            packageId = "either";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "use_std" ];
+          "use_std" = [ "use_alloc" "either/use_std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "use_alloc" "use_std" ];
+      };
+      "itoa" = rec {
+        crateName = "itoa";
+        version = "1.0.6";
+        edition = "2018";
+        sha256 = "19jc2sa3wvdc29zhgbwf3bayikq4rq18n20dbyg9ahd4hbsxjfj5";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        features = {
+          "no-panic" = [ "dep:no-panic" ];
+        };
+      };
+      "jobserver" = rec {
+        crateName = "jobserver";
+        version = "0.1.26";
+        edition = "2018";
+        sha256 = "1hkprvh1zp5s3qwjjwwhw7rcpivczcbf6q60rcxr0m8158hzsv4k";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+        ];
+
+      };
+      "js-sys" = rec {
+        crateName = "js-sys";
+        version = "0.3.61";
+        edition = "2018";
+        sha256 = "0c075apyc5fxp2sbgr87qcvq53pcjxmp05l47lzlhpn5a0hxwpa4";
+        authors = [
+          "The wasm-bindgen Developers"
+        ];
+        dependencies = [
+          {
+            name = "wasm-bindgen";
+            packageId = "wasm-bindgen";
+          }
+        ];
+
+      };
+      "lazy_static" = rec {
+        crateName = "lazy_static";
+        version = "1.4.0";
+        edition = "2015";
+        sha256 = "0in6ikhw8mgl33wjv6q6xfrb5b9jr16q8ygjy803fay4zcisvaz2";
+        authors = [
+          "Marvin Löbel <loebel.marvin@gmail.com>"
+        ];
+        features = {
+          "spin" = [ "dep:spin" ];
+          "spin_no_std" = [ "spin" ];
+        };
+      };
+      "lexical-core" = rec {
+        crateName = "lexical-core";
+        version = "0.8.5";
+        edition = "2018";
+        sha256 = "0ihf0x3vrk25fq3bv9q35m0xax0wmvwkh0j0pjm2yk4ddvh5vpic";
+        authors = [
+          "Alex Huszagh <ahuszagh@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "lexical-parse-float";
+            packageId = "lexical-parse-float";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "lexical-parse-integer";
+            packageId = "lexical-parse-integer";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "lexical-util";
+            packageId = "lexical-util";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "lexical-write-float";
+            packageId = "lexical-write-float";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "lexical-write-integer";
+            packageId = "lexical-write-integer";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "compact" = [ "lexical-write-integer/compact" "lexical-write-float/compact" "lexical-parse-integer/compact" "lexical-parse-float/compact" ];
+          "default" = [ "std" "write-integers" "write-floats" "parse-integers" "parse-floats" ];
+          "f128" = [ "lexical-util/f128" "lexical-parse-float/f128" "lexical-write-float/f128" ];
+          "f16" = [ "lexical-util/f16" "lexical-parse-float/f16" "lexical-write-float/f16" ];
+          "format" = [ "lexical-util/format" "lexical-parse-integer/format" "lexical-parse-float/format" "lexical-write-integer/format" "lexical-write-float/format" ];
+          "lexical-parse-float" = [ "dep:lexical-parse-float" ];
+          "lexical-parse-integer" = [ "dep:lexical-parse-integer" ];
+          "lexical-write-float" = [ "dep:lexical-write-float" ];
+          "lexical-write-integer" = [ "dep:lexical-write-integer" ];
+          "lint" = [ "lexical-util/lint" "lexical-write-integer/lint" "lexical-write-float/lint" "lexical-parse-integer/lint" "lexical-parse-float/lint" ];
+          "nightly" = [ "lexical-write-integer/nightly" "lexical-write-float/nightly" "lexical-parse-integer/nightly" "lexical-parse-float/nightly" ];
+          "parse-floats" = [ "lexical-parse-float" "parse" "floats" ];
+          "parse-integers" = [ "lexical-parse-integer" "parse" "integers" ];
+          "power-of-two" = [ "lexical-util/power-of-two" "lexical-write-integer/power-of-two" "lexical-write-float/power-of-two" "lexical-parse-integer/power-of-two" "lexical-parse-float/power-of-two" ];
+          "radix" = [ "lexical-util/radix" "lexical-write-integer/radix" "lexical-write-float/radix" "lexical-parse-integer/radix" "lexical-parse-float/radix" ];
+          "safe" = [ "lexical-write-integer/safe" "lexical-write-float/safe" "lexical-parse-integer/safe" "lexical-parse-float/safe" ];
+          "std" = [ "lexical-util/std" "lexical-write-integer/std" "lexical-write-float/std" "lexical-parse-integer/std" "lexical-parse-float/std" ];
+          "write-floats" = [ "lexical-write-float" "write" "floats" ];
+          "write-integers" = [ "lexical-write-integer" "write" "integers" ];
+        };
+        resolvedDefaultFeatures = [ "default" "floats" "format" "integers" "lexical-parse-float" "lexical-parse-integer" "lexical-write-float" "lexical-write-integer" "parse" "parse-floats" "parse-integers" "std" "write" "write-floats" "write-integers" ];
+      };
+      "lexical-parse-float" = rec {
+        crateName = "lexical-parse-float";
+        version = "0.8.5";
+        edition = "2018";
+        sha256 = "0py0gp8hlzcrlvjqmqlpl2v1as65iiqxq2xsabxvhc01pmg3lfv8";
+        authors = [
+          "Alex Huszagh <ahuszagh@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "lexical-parse-integer";
+            packageId = "lexical-parse-integer";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "lexical-util";
+            packageId = "lexical-util";
+            usesDefaultFeatures = false;
+            features = [ "parse-floats" ];
+          }
+          {
+            name = "static_assertions";
+            packageId = "static_assertions";
+          }
+        ];
+        features = {
+          "compact" = [ "lexical-util/compact" "lexical-parse-integer/compact" ];
+          "default" = [ "std" ];
+          "f128" = [ "lexical-util/f128" ];
+          "f16" = [ "lexical-util/f16" ];
+          "format" = [ "lexical-util/format" "lexical-parse-integer/format" ];
+          "lint" = [ "lexical-util/lint" "lexical-parse-integer/lint" ];
+          "nightly" = [ "lexical-parse-integer/nightly" ];
+          "power-of-two" = [ "lexical-util/power-of-two" "lexical-parse-integer/power-of-two" ];
+          "radix" = [ "lexical-util/radix" "lexical-parse-integer/radix" "power-of-two" ];
+          "safe" = [ "lexical-parse-integer/safe" ];
+          "std" = [ "lexical-util/std" "lexical-parse-integer/std" ];
+        };
+        resolvedDefaultFeatures = [ "format" "std" ];
+      };
+      "lexical-parse-integer" = rec {
+        crateName = "lexical-parse-integer";
+        version = "0.8.6";
+        edition = "2018";
+        sha256 = "1sayji3mpvb2xsjq56qcq3whfz8px9a6fxk5v7v15hyhbr4982bd";
+        authors = [
+          "Alex Huszagh <ahuszagh@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "lexical-util";
+            packageId = "lexical-util";
+            usesDefaultFeatures = false;
+            features = [ "parse-integers" ];
+          }
+          {
+            name = "static_assertions";
+            packageId = "static_assertions";
+          }
+        ];
+        features = {
+          "compact" = [ "lexical-util/compact" ];
+          "default" = [ "std" ];
+          "format" = [ "lexical-util/format" ];
+          "lint" = [ "lexical-util/lint" ];
+          "power-of-two" = [ "lexical-util/power-of-two" ];
+          "radix" = [ "lexical-util/radix" "power-of-two" ];
+          "std" = [ "lexical-util/std" ];
+        };
+        resolvedDefaultFeatures = [ "format" "std" ];
+      };
+      "lexical-util" = rec {
+        crateName = "lexical-util";
+        version = "0.8.5";
+        edition = "2018";
+        sha256 = "1z73qkv7yxhsbc4aiginn1dqmsj8jarkrdlyxc88g2gz2vzvjmaj";
+        authors = [
+          "Alex Huszagh <ahuszagh@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "static_assertions";
+            packageId = "static_assertions";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "f128" = [ "floats" ];
+          "f16" = [ "floats" ];
+          "parse-floats" = [ "parse" "floats" ];
+          "parse-integers" = [ "parse" "integers" ];
+          "radix" = [ "power-of-two" ];
+          "write-floats" = [ "write" "floats" ];
+          "write-integers" = [ "write" "integers" ];
+        };
+        resolvedDefaultFeatures = [ "floats" "format" "integers" "parse" "parse-floats" "parse-integers" "std" "write" "write-floats" "write-integers" ];
+      };
+      "lexical-write-float" = rec {
+        crateName = "lexical-write-float";
+        version = "0.8.5";
+        edition = "2018";
+        sha256 = "0qk825l0csvnksh9sywb51996cjc2bylq6rxjaiha7sqqjhvmjmc";
+        authors = [
+          "Alex Huszagh <ahuszagh@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "lexical-util";
+            packageId = "lexical-util";
+            usesDefaultFeatures = false;
+            features = [ "write-floats" ];
+          }
+          {
+            name = "lexical-write-integer";
+            packageId = "lexical-write-integer";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "static_assertions";
+            packageId = "static_assertions";
+          }
+        ];
+        features = {
+          "compact" = [ "lexical-util/compact" "lexical-write-integer/compact" ];
+          "default" = [ "std" ];
+          "f128" = [ "lexical-util/f128" ];
+          "f16" = [ "lexical-util/f16" ];
+          "format" = [ "lexical-util/format" ];
+          "lint" = [ "lexical-util/lint" "lexical-write-integer/lint" ];
+          "nightly" = [ "lexical-write-integer/nightly" ];
+          "power-of-two" = [ "lexical-util/power-of-two" "lexical-write-integer/power-of-two" ];
+          "radix" = [ "lexical-util/radix" "lexical-write-integer/radix" "power-of-two" ];
+          "safe" = [ "lexical-write-integer/safe" ];
+          "std" = [ "lexical-util/std" "lexical-write-integer/std" ];
+        };
+        resolvedDefaultFeatures = [ "format" "std" ];
+      };
+      "lexical-write-integer" = rec {
+        crateName = "lexical-write-integer";
+        version = "0.8.5";
+        edition = "2018";
+        sha256 = "0ii4hmvqrg6pd4j9y1pkhkp0nw2wpivjzmljh6v6ca22yk8z7dp1";
+        authors = [
+          "Alex Huszagh <ahuszagh@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "lexical-util";
+            packageId = "lexical-util";
+            usesDefaultFeatures = false;
+            features = [ "write-integers" ];
+          }
+          {
+            name = "static_assertions";
+            packageId = "static_assertions";
+          }
+        ];
+        features = {
+          "compact" = [ "lexical-util/compact" ];
+          "default" = [ "std" ];
+          "format" = [ "lexical-util/format" ];
+          "lint" = [ "lexical-util/lint" ];
+          "power-of-two" = [ "lexical-util/power-of-two" ];
+          "radix" = [ "lexical-util/radix" "power-of-two" ];
+          "std" = [ "lexical-util/std" ];
+        };
+        resolvedDefaultFeatures = [ "format" "std" ];
+      };
+      "libc" = rec {
+        crateName = "libc";
+        version = "0.2.143";
+        edition = "2015";
+        sha256 = "0940zabsl3gm2jr03mg2ign4rnakdd4rp5hfhjzddic57s4hghpd";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        features = {
+          "default" = [ "std" ];
+          "rustc-dep-of-std" = [ "align" "rustc-std-workspace-core" ];
+          "rustc-std-workspace-core" = [ "dep:rustc-std-workspace-core" ];
+          "use_std" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "extra_traits" "std" ];
+      };
+      "libm" = rec {
+        crateName = "libm";
+        version = "0.2.6";
+        edition = "2018";
+        sha256 = "1ywg7jfcgfv4jypxi3f6rpf7n9509ky695bfzy1fqhms7ymhi09l";
+        authors = [
+          "Jorge Aparicio <jorge@japaric.io>"
+        ];
+        features = {
+          "musl-reference-tests" = [ "rand" ];
+          "rand" = [ "dep:rand" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "linux-raw-sys" = rec {
+        crateName = "linux-raw-sys";
+        version = "0.3.7";
+        edition = "2018";
+        sha256 = "17s7qr5h82blrxy29014zzhr30jcxcjc8r16v2p31rzcfal7xsgc";
+        authors = [
+          "Dan Gohman <dev@sunfishcode.online>"
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "default" = [ "std" "general" "errno" ];
+          "rustc-dep-of-std" = [ "core" "compiler_builtins" "no_std" ];
+        };
+        resolvedDefaultFeatures = [ "errno" "general" "ioctl" "no_std" ];
+      };
+      "lock_api" = rec {
+        crateName = "lock_api";
+        version = "0.4.9";
+        edition = "2018";
+        sha256 = "1py41vk243hwk345nhkn5nw0bd4m03gzjmprdjqq6rg5dwv12l23";
+        authors = [
+          "Amanieu d'Antras <amanieu@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "scopeguard";
+            packageId = "scopeguard";
+            usesDefaultFeatures = false;
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = {
+          "owning_ref" = [ "dep:owning_ref" ];
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "log" = rec {
+        crateName = "log";
+        version = "0.4.17";
+        edition = "2015";
+        sha256 = "0biqlaaw1lsr8bpnmbcc0fvgjj34yy79ghqzyi0ali7vgil2xcdb";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+        ];
+        features = {
+          "kv_unstable" = [ "value-bag" ];
+          "kv_unstable_serde" = [ "kv_unstable_std" "value-bag/serde" "serde" ];
+          "kv_unstable_std" = [ "std" "kv_unstable" "value-bag/error" ];
+          "kv_unstable_sval" = [ "kv_unstable" "value-bag/sval" "sval" ];
+          "serde" = [ "dep:serde" ];
+          "sval" = [ "dep:sval" ];
+          "value-bag" = [ "dep:value-bag" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "matchit" = rec {
+        crateName = "matchit";
+        version = "0.7.0";
+        edition = "2021";
+        sha256 = "0h7a1a57wamz0305dipj20shv2b5dw47jjp6dsgfaxmpmznlhwmq";
+        authors = [
+          "Ibraheem Ahmed <ibraheem@ibraheem.ca>"
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "memchr" = rec {
+        crateName = "memchr";
+        version = "2.5.0";
+        edition = "2018";
+        sha256 = "0vanfk5mzs1g1syqnj03q8n0syggnhn55dq535h2wxr7rwpfbzrd";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+          "bluss"
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "default" = [ "std" ];
+          "libc" = [ "dep:libc" ];
+          "rustc-dep-of-std" = [ "core" "compiler_builtins" ];
+          "use_std" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "memoffset" = rec {
+        crateName = "memoffset";
+        version = "0.8.0";
+        edition = "2015";
+        sha256 = "1qcdic88dhgw76pafgndpz04pig8il4advq978mxdxdwrydp276n";
+        authors = [
+          "Gilad Naaman <gilad.naaman@gmail.com>"
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "mime" = rec {
+        crateName = "mime";
+        version = "0.3.17";
+        edition = "2015";
+        sha256 = "16hkibgvb9klh0w0jk5crr5xv90l3wlf77ggymzjmvl1818vnxv8";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+
+      };
+      "mio" = rec {
+        crateName = "mio";
+        version = "0.8.6";
+        edition = "2018";
+        sha256 = "1ygx5chq81k3vk2bx722xwcwf2qydmm337jsnijgzd7mxx39m7av";
+        authors = [
+          "Carl Lerche <me@carllerche.com>"
+          "Thomas de Zeeuw <thomasdezeeuw@gmail.com>"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: ("wasi" == target."os");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "log";
+            packageId = "log";
+          }
+          {
+            name = "wasi";
+            packageId = "wasi";
+            target = { target, features }: ("wasi" == target."os");
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.45.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_Foundation" "Win32_Networking_WinSock" "Win32_Storage_FileSystem" "Win32_System_IO" "Win32_System_WindowsProgramming" ];
+          }
+        ];
+        features = {
+          "os-ext" = [ "os-poll" "windows-sys/Win32_System_Pipes" "windows-sys/Win32_Security" ];
+        };
+        resolvedDefaultFeatures = [ "default" "net" "os-ext" "os-poll" ];
+      };
+      "multimap" = rec {
+        crateName = "multimap";
+        version = "0.8.3";
+        edition = "2015";
+        sha256 = "0sicyz4n500vdhgcxn4g8jz97cp1ijir1rnbgph3pmx9ckz4dkp5";
+        authors = [
+          "Håvar Nøvik <havar.novik@gmail.com>"
+        ];
+        features = {
+          "default" = [ "serde_impl" ];
+          "serde" = [ "dep:serde" ];
+          "serde_impl" = [ "serde" ];
+        };
+      };
+      "nibble_vec" = rec {
+        crateName = "nibble_vec";
+        version = "0.1.0";
+        edition = "2018";
+        sha256 = "0hsdp3s724s30hkqz74ky6sqnadhp2xwcj1n1hzy4vzkz4yxi9bp";
+        authors = [
+          "Michael Sproul <micsproul@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "smallvec";
+            packageId = "smallvec";
+          }
+        ];
+
+      };
+      "nix" = rec {
+        crateName = "nix";
+        version = "0.25.1";
+        edition = "2018";
+        sha256 = "1r4vyp5g1lxzpig31bkrhxdf2bggb4nvk405x5gngzfvwxqgyipk";
+        authors = [
+          "The nix-rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            features = [ "extra_traits" ];
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = {
+          "aio" = [ "pin-utils" ];
+          "default" = [ "acct" "aio" "dir" "env" "event" "feature" "fs" "hostname" "inotify" "ioctl" "kmod" "mman" "mount" "mqueue" "net" "personality" "poll" "process" "pthread" "ptrace" "quota" "reboot" "resource" "sched" "signal" "socket" "term" "time" "ucontext" "uio" "user" "zerocopy" ];
+          "dir" = [ "fs" ];
+          "memoffset" = [ "dep:memoffset" ];
+          "mount" = [ "uio" ];
+          "mqueue" = [ "fs" ];
+          "net" = [ "socket" ];
+          "pin-utils" = [ "dep:pin-utils" ];
+          "ptrace" = [ "process" ];
+          "sched" = [ "process" ];
+          "signal" = [ "process" ];
+          "socket" = [ "memoffset" ];
+          "ucontext" = [ "signal" ];
+          "user" = [ "feature" ];
+          "zerocopy" = [ "fs" "uio" ];
+        };
+        resolvedDefaultFeatures = [ "fs" "ioctl" "poll" "process" "signal" "term" ];
+      };
+      "nix-cli" = rec {
+        crateName = "nix-cli";
+        version = "0.1.0";
+        edition = "2021";
+        crateBin = [
+          {
+            name = "nix-cli";
+            path = "src/main.rs";
+            requiredFeatures = [ ];
+          }
+          {
+            name = "nix-store";
+            path = "src/bin/nix-store.rs";
+            requiredFeatures = [ ];
+          }
+        ];
+        # We can't filter paths with references in Nix 2.4
+        # See https://github.com/NixOS/nix/issues/5410
+        src =
+          if (lib.versionOlder builtins.nixVersion "2.4pre20211007")
+          then lib.cleanSourceWith { filter = sourceFilter; src = ./nix_cli; }
+          else ./nix_cli;
+        dependencies = [
+          {
+            name = "clap";
+            packageId = "clap 4.2.7";
+          }
+          {
+            name = "tempfile";
+            packageId = "tempfile";
+          }
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "integration_tests" ];
+      };
+      "nix-compat" = rec {
+        crateName = "nix-compat";
+        version = "0.1.0";
+        edition = "2021";
+        # We can't filter paths with references in Nix 2.4
+        # See https://github.com/NixOS/nix/issues/5410
+        src =
+          if (lib.versionOlder builtins.nixVersion "2.4pre20211007")
+          then lib.cleanSourceWith { filter = sourceFilter; src = ./nix-compat; }
+          else ./nix-compat;
+        dependencies = [
+          {
+            name = "anyhow";
+            packageId = "anyhow";
+          }
+          {
+            name = "data-encoding";
+            packageId = "data-encoding";
+          }
+          {
+            name = "glob";
+            packageId = "glob";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "derive" ];
+          }
+          {
+            name = "serde_json";
+            packageId = "serde_json";
+          }
+          {
+            name = "sha2";
+            packageId = "sha2 0.10.6";
+          }
+          {
+            name = "thiserror";
+            packageId = "thiserror";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "serde_json";
+            packageId = "serde_json";
+          }
+          {
+            name = "test-case";
+            packageId = "test-case";
+          }
+          {
+            name = "test-generator";
+            packageId = "test-generator";
+          }
+        ];
+
+      };
+      "nom8" = rec {
+        crateName = "nom8";
+        version = "0.2.0";
+        edition = "2018";
+        sha256 = "1y6jzabxyrl05vxnh63r66ac2fh0symg5fnynxm4ii3zkif580df";
+        dependencies = [
+          {
+            name = "memchr";
+            packageId = "memchr";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "std" = [ "alloc" "memchr/std" ];
+          "unstable-doc" = [ "alloc" "std" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "std" ];
+      };
+      "nu-ansi-term" = rec {
+        crateName = "nu-ansi-term";
+        version = "0.46.0";
+        edition = "2018";
+        sha256 = "115sywxh53p190lyw97alm14nc004qj5jm5lvdj608z84rbida3p";
+        authors = [
+          "ogham@bsago.me"
+          "Ryan Scheel (Havvy) <ryan.havvy@gmail.com>"
+          "Josh Triplett <josh@joshtriplett.org>"
+          "The Nushell Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "overload";
+            packageId = "overload";
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: ("windows" == target."os");
+            features = [ "consoleapi" "errhandlingapi" "fileapi" "handleapi" "processenv" ];
+          }
+        ];
+        features = {
+          "derive_serde_style" = [ "serde" ];
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "num-traits" = rec {
+        crateName = "num-traits";
+        version = "0.2.15";
+        edition = "2015";
+        sha256 = "1kfdqqw2ndz0wx2j75v9nbjx7d3mh3150zs4p5595y02rwsdx3jp";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "libm";
+            packageId = "libm";
+            optional = true;
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "libm" = [ "dep:libm" ];
+        };
+        resolvedDefaultFeatures = [ "default" "libm" "std" ];
+      };
+      "num_cpus" = rec {
+        crateName = "num_cpus";
+        version = "1.15.0";
+        edition = "2015";
+        sha256 = "0fsrjy3arnbcl41vz0gppya8d7d24cpkjgfflr3v8pivl4nrxb0g";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "hermit-abi";
+            packageId = "hermit-abi 0.2.6";
+            target = { target, features }: ((("x86_64" == target."arch") || ("aarch64" == target."arch")) && ("hermit" == target."os"));
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (!(target."windows" or false));
+          }
+        ];
+
+      };
+      "once_cell" = rec {
+        crateName = "once_cell";
+        version = "1.17.1";
+        edition = "2021";
+        sha256 = "1lrsy9c5ikf2iwxr4iwgd3rlq9mg8alh0np1g8abnvp1k4151rdp";
+        authors = [
+          "Aleksey Kladov <aleksey.kladov@gmail.com>"
+        ];
+        features = {
+          "alloc" = [ "race" ];
+          "atomic-polyfill" = [ "critical-section" ];
+          "atomic_polyfill" = [ "dep:atomic_polyfill" ];
+          "critical-section" = [ "critical_section" "atomic_polyfill" ];
+          "critical_section" = [ "dep:critical_section" ];
+          "default" = [ "std" ];
+          "parking_lot" = [ "parking_lot_core" ];
+          "parking_lot_core" = [ "dep:parking_lot_core" ];
+          "std" = [ "alloc" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "race" "std" ];
+      };
+      "oorandom" = rec {
+        crateName = "oorandom";
+        version = "11.1.3";
+        edition = "2018";
+        sha256 = "0xdm4vd89aiwnrk1xjwzklnchjqvib4klcihlc2bsd4x50mbrc8a";
+        authors = [
+          "Simon Heath <icefox@dreamquest.io>"
+        ];
+
+      };
+      "opaque-debug" = rec {
+        crateName = "opaque-debug";
+        version = "0.2.3";
+        edition = "2015";
+        sha256 = "172j6bs8ndclqxa2m64qc0y1772rr73g4l9fg2svscgicnbfff98";
+        authors = [
+          "RustCrypto Developers"
+        ];
+
+      };
+      "os_str_bytes" = rec {
+        crateName = "os_str_bytes";
+        version = "6.5.0";
+        edition = "2021";
+        sha256 = "0rz2711gl575ng6vm9a97q42wqnf4wk1165wn221jb8gn17z9vff";
+        authors = [
+          "dylni"
+        ];
+        features = {
+          "default" = [ "memchr" "raw_os_str" ];
+          "memchr" = [ "dep:memchr" ];
+          "print_bytes" = [ "dep:print_bytes" ];
+          "uniquote" = [ "dep:uniquote" ];
+        };
+        resolvedDefaultFeatures = [ "raw_os_str" ];
+      };
+      "output_vt100" = rec {
+        crateName = "output_vt100";
+        version = "0.1.3";
+        edition = "2018";
+        sha256 = "0rpvpiq7gkyvvwyp9sk0zxhbk99ldlrv5q3ycr03wkmbxgx270k2";
+        authors = [
+          "Phuntsok Drak-pa <phundrak@phundrak.fr>"
+        ];
+        dependencies = [
+          {
+            name = "winapi";
+            packageId = "winapi";
+            features = [ "winuser" "winbase" "consoleapi" "processenv" ];
+          }
+        ];
+
+      };
+      "overload" = rec {
+        crateName = "overload";
+        version = "0.1.1";
+        edition = "2018";
+        sha256 = "0fdgbaqwknillagy1xq7xfgv60qdbk010diwl7s1p0qx7hb16n5i";
+        authors = [
+          "Daniel Salvadori <danaugrs@gmail.com>"
+        ];
+
+      };
+      "parking_lot" = rec {
+        crateName = "parking_lot";
+        version = "0.11.2";
+        edition = "2018";
+        sha256 = "16gzf41bxmm10x82bla8d6wfppy9ym3fxsmdjyvn61m66s0bf5vx";
+        authors = [
+          "Amanieu d'Antras <amanieu@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "instant";
+            packageId = "instant";
+          }
+          {
+            name = "lock_api";
+            packageId = "lock_api";
+          }
+          {
+            name = "parking_lot_core";
+            packageId = "parking_lot_core";
+          }
+        ];
+        features = {
+          "arc_lock" = [ "lock_api/arc_lock" ];
+          "deadlock_detection" = [ "parking_lot_core/deadlock_detection" ];
+          "nightly" = [ "parking_lot_core/nightly" "lock_api/nightly" ];
+          "owning_ref" = [ "lock_api/owning_ref" ];
+          "serde" = [ "lock_api/serde" ];
+          "stdweb" = [ "instant/stdweb" ];
+          "wasm-bindgen" = [ "instant/wasm-bindgen" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "parking_lot_core" = rec {
+        crateName = "parking_lot_core";
+        version = "0.8.6";
+        edition = "2018";
+        sha256 = "1p2nfcbr0b9lm9rglgm28k6mwyjwgm4knipsmqbgqaxdy3kcz8k0";
+        authors = [
+          "Amanieu d'Antras <amanieu@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "instant";
+            packageId = "instant";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "redox_syscall";
+            packageId = "redox_syscall 0.2.16";
+            target = { target, features }: ("redox" == target."os");
+          }
+          {
+            name = "smallvec";
+            packageId = "smallvec";
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "winnt" "ntstatus" "minwindef" "winerror" "winbase" "errhandlingapi" "handleapi" ];
+          }
+        ];
+        features = {
+          "backtrace" = [ "dep:backtrace" ];
+          "deadlock_detection" = [ "petgraph" "thread-id" "backtrace" ];
+          "petgraph" = [ "dep:petgraph" ];
+          "thread-id" = [ "dep:thread-id" ];
+        };
+      };
+      "path-clean" = rec {
+        crateName = "path-clean";
+        version = "0.1.0";
+        edition = "2015";
+        sha256 = "1pcgqxw0mgg3ha5hi5xkjhyjf488bw5rw1g3qlr9awbq4szh3fpc";
+        authors = [
+          "Dan Reeves <hey@danreev.es>"
+        ];
+
+      };
+      "percent-encoding" = rec {
+        crateName = "percent-encoding";
+        version = "2.2.0";
+        edition = "2018";
+        sha256 = "13nrpp6r1f4k14viksga3094krcrxgv4b42kqbriy63k7ln5g327";
+        authors = [
+          "The rust-url developers"
+        ];
+        features = {
+          "default" = [ "alloc" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" ];
+      };
+      "petgraph" = rec {
+        crateName = "petgraph";
+        version = "0.6.3";
+        edition = "2018";
+        sha256 = "1912xw827flj8mzqm62jcbg0cv54qfhzm48c13ilzr9px67d5msd";
+        authors = [
+          "bluss"
+          "mitchmindtree"
+        ];
+        dependencies = [
+          {
+            name = "fixedbitset";
+            packageId = "fixedbitset";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "indexmap";
+            packageId = "indexmap";
+            features = [ "std" ];
+          }
+        ];
+        features = {
+          "all" = [ "unstable" "quickcheck" "matrix_graph" "stable_graph" "graphmap" ];
+          "default" = [ "graphmap" "stable_graph" "matrix_graph" ];
+          "quickcheck" = [ "dep:quickcheck" ];
+          "serde" = [ "dep:serde" ];
+          "serde-1" = [ "serde" "serde_derive" ];
+          "serde_derive" = [ "dep:serde_derive" ];
+          "unstable" = [ "generate" ];
+        };
+      };
+      "pin-project" = rec {
+        crateName = "pin-project";
+        version = "1.0.12";
+        edition = "2018";
+        sha256 = "1k3f9jkia3idxl2pqxamszwnl89dk52fa4jqj3p7zmmwnq4scadd";
+        dependencies = [
+          {
+            name = "pin-project-internal";
+            packageId = "pin-project-internal";
+          }
+        ];
+
+      };
+      "pin-project-internal" = rec {
+        crateName = "pin-project-internal";
+        version = "1.0.12";
+        edition = "2018";
+        sha256 = "0maa6icn7rdfy4xvgfaq7m7bwpw9f19wg76f1ncsiixd0lgdp6q6";
+        procMacro = true;
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "full" "visit-mut" ];
+          }
+        ];
+
+      };
+      "pin-project-lite" = rec {
+        crateName = "pin-project-lite";
+        version = "0.2.9";
+        edition = "2018";
+        sha256 = "05n1z851l356hpgqadw4ar64mjanaxq1qlwqsf2k05ziq8xax9z0";
+
+      };
+      "pin-utils" = rec {
+        crateName = "pin-utils";
+        version = "0.1.0";
+        edition = "2018";
+        sha256 = "117ir7vslsl2z1a7qzhws4pd01cg2d3338c47swjyvqv2n60v1wb";
+        authors = [
+          "Josef Brandl <mail@josefbrandl.de>"
+        ];
+
+      };
+      "plotters" = rec {
+        crateName = "plotters";
+        version = "0.3.4";
+        edition = "2018";
+        sha256 = "15xzbxnjcfsaf8lac846lgi4xmn9k18m9k8gqm35aaa2wqwvcf15";
+        authors = [
+          "Hao Hou <haohou302@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "num-traits";
+            packageId = "num-traits";
+          }
+          {
+            name = "plotters-backend";
+            packageId = "plotters-backend";
+          }
+          {
+            name = "plotters-svg";
+            packageId = "plotters-svg";
+            optional = true;
+          }
+          {
+            name = "wasm-bindgen";
+            packageId = "wasm-bindgen";
+            target = { target, features }: (("wasm32" == target."arch") && (!("wasi" == target."os")));
+          }
+          {
+            name = "web-sys";
+            packageId = "web-sys";
+            target = { target, features }: (("wasm32" == target."arch") && (!("wasi" == target."os")));
+            features = [ "Document" "DomRect" "Element" "HtmlElement" "Node" "Window" "HtmlCanvasElement" "CanvasRenderingContext2d" ];
+          }
+        ];
+        features = {
+          "all_elements" = [ "errorbar" "candlestick" "boxplot" "histogram" ];
+          "all_series" = [ "area_series" "line_series" "point_series" "surface_series" ];
+          "bitmap_backend" = [ "plotters-bitmap" "ttf" ];
+          "bitmap_encoder" = [ "plotters-bitmap/image_encoder" ];
+          "bitmap_gif" = [ "plotters-bitmap/gif_backend" ];
+          "chrono" = [ "dep:chrono" ];
+          "datetime" = [ "chrono" ];
+          "default" = [ "bitmap_backend" "bitmap_encoder" "bitmap_gif" "svg_backend" "chrono" "ttf" "image" "deprecated_items" "all_series" "all_elements" "full_palette" ];
+          "evcxr" = [ "svg_backend" ];
+          "evcxr_bitmap" = [ "evcxr" "bitmap_backend" "plotters-svg/bitmap_encoder" ];
+          "font-kit" = [ "dep:font-kit" ];
+          "fontconfig-dlopen" = [ "font-kit/source-fontconfig-dlopen" ];
+          "image" = [ "dep:image" ];
+          "lazy_static" = [ "dep:lazy_static" ];
+          "pathfinder_geometry" = [ "dep:pathfinder_geometry" ];
+          "plotters-bitmap" = [ "dep:plotters-bitmap" ];
+          "plotters-svg" = [ "dep:plotters-svg" ];
+          "svg_backend" = [ "plotters-svg" ];
+          "ttf" = [ "font-kit" "ttf-parser" "lazy_static" "pathfinder_geometry" ];
+          "ttf-parser" = [ "dep:ttf-parser" ];
+        };
+        resolvedDefaultFeatures = [ "area_series" "line_series" "plotters-svg" "svg_backend" ];
+      };
+      "plotters-backend" = rec {
+        crateName = "plotters-backend";
+        version = "0.3.4";
+        edition = "2018";
+        sha256 = "0hl1x8dqrzsjw1vabyw48gzp7g6z8rlyjqjc4b0wvzl1cdhjhchr";
+        authors = [
+          "Hao Hou <haohou302@gmail.com>"
+        ];
+
+      };
+      "plotters-svg" = rec {
+        crateName = "plotters-svg";
+        version = "0.3.3";
+        edition = "2018";
+        sha256 = "0vx5wmm5mxip3fm4l67l3wcvv3jwph4c70zpd3kdmqdab4kiva7r";
+        authors = [
+          "Hao Hou <haohou302@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "plotters-backend";
+            packageId = "plotters-backend";
+          }
+        ];
+        features = {
+          "bitmap_encoder" = [ "image" ];
+          "image" = [ "dep:image" ];
+        };
+      };
+      "ppv-lite86" = rec {
+        crateName = "ppv-lite86";
+        version = "0.2.17";
+        edition = "2018";
+        sha256 = "1pp6g52aw970adv3x2310n7glqnji96z0a9wiamzw89ibf0ayh2v";
+        authors = [
+          "The CryptoCorrosion Contributors"
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "simd" "std" ];
+      };
+      "pretty_assertions" = rec {
+        crateName = "pretty_assertions";
+        version = "1.3.0";
+        edition = "2018";
+        sha256 = "0mgp1ajl3fdc55h989ph48znnk86m41j9dqnpg80yy5a435rnpm2";
+        authors = [
+          "Colin Kiegel <kiegel@gmx.de>"
+          "Florent Fayolle <florent.fayolle69@gmail.com>"
+          "Tom Milligan <code@tommilligan.net>"
+        ];
+        dependencies = [
+          {
+            name = "ctor";
+            packageId = "ctor";
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "diff";
+            packageId = "diff";
+          }
+          {
+            name = "output_vt100";
+            packageId = "output_vt100";
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "yansi";
+            packageId = "yansi";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "prettyplease" = rec {
+        crateName = "prettyplease";
+        version = "0.1.25";
+        edition = "2021";
+        sha256 = "11lskniv8pf8y8bn4dc3nmjapfhnibxbm5gamp2ad9qna3lld1kc";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            usesDefaultFeatures = false;
+            features = [ "full" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            usesDefaultFeatures = false;
+            features = [ "parsing" ];
+          }
+        ];
+        features = {
+          "verbatim" = [ "syn/parsing" ];
+        };
+      };
+      "proc-macro-error" = rec {
+        crateName = "proc-macro-error";
+        version = "1.0.4";
+        edition = "2018";
+        sha256 = "1373bhxaf0pagd8zkyd03kkx6bchzf6g0dkwrwzsnal9z47lj9fs";
+        authors = [
+          "CreepySkeleton <creepy-skeleton@yandex.ru>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro-error-attr";
+            packageId = "proc-macro-error-attr";
+          }
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "version_check";
+            packageId = "version_check";
+          }
+        ];
+        features = {
+          "default" = [ "syn-error" ];
+          "syn" = [ "dep:syn" ];
+          "syn-error" = [ "syn" ];
+        };
+        resolvedDefaultFeatures = [ "default" "syn" "syn-error" ];
+      };
+      "proc-macro-error-attr" = rec {
+        crateName = "proc-macro-error-attr";
+        version = "1.0.4";
+        edition = "2018";
+        sha256 = "0sgq6m5jfmasmwwy8x4mjygx5l7kp8s4j60bv25ckv2j1qc41gm1";
+        procMacro = true;
+        authors = [
+          "CreepySkeleton <creepy-skeleton@yandex.ru>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "version_check";
+            packageId = "version_check";
+          }
+        ];
+
+      };
+      "proc-macro2 0.4.30" = rec {
+        crateName = "proc-macro2";
+        version = "0.4.30";
+        edition = "2015";
+        sha256 = "0nd71fl24sys066jrha6j7i34nfkjv44yzw8yww9742wmc8j0gfg";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "unicode-xid";
+            packageId = "unicode-xid";
+          }
+        ];
+        features = {
+          "default" = [ "proc-macro" ];
+        };
+        resolvedDefaultFeatures = [ "default" "proc-macro" ];
+      };
+      "proc-macro2 1.0.56" = rec {
+        crateName = "proc-macro2";
+        version = "1.0.56";
+        edition = "2018";
+        sha256 = "0ddlk2c7s9c0fhmf8cd0wikayicv9xrm9ck9vzgg9w86rnqbsqrb";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "unicode-ident";
+            packageId = "unicode-ident";
+          }
+        ];
+        features = {
+          "default" = [ "proc-macro" ];
+        };
+        resolvedDefaultFeatures = [ "default" "proc-macro" ];
+      };
+      "proptest" = rec {
+        crateName = "proptest";
+        version = "1.1.0";
+        edition = "2018";
+        sha256 = "0w5s3wwchd3i6m15lsbsk3r3y25d1gwn13m6a185ks8w06cbiw99";
+        authors = [
+          "Jason Lingle"
+        ];
+        dependencies = [
+          {
+            name = "bit-set";
+            packageId = "bit-set";
+            optional = true;
+          }
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "byteorder";
+            packageId = "byteorder";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+            optional = true;
+          }
+          {
+            name = "num-traits";
+            packageId = "num-traits";
+            usesDefaultFeatures = false;
+            features = [ "libm" ];
+          }
+          {
+            name = "quick-error";
+            packageId = "quick-error 2.0.1";
+            optional = true;
+          }
+          {
+            name = "rand";
+            packageId = "rand 0.8.5";
+            usesDefaultFeatures = false;
+            features = [ "alloc" ];
+          }
+          {
+            name = "rand_chacha";
+            packageId = "rand_chacha";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "rand_xorshift";
+            packageId = "rand_xorshift";
+          }
+          {
+            name = "regex-syntax";
+            packageId = "regex-syntax 0.6.29";
+            optional = true;
+          }
+          {
+            name = "rusty-fork";
+            packageId = "rusty-fork";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "tempfile";
+            packageId = "tempfile";
+            optional = true;
+          }
+          {
+            name = "unarray";
+            packageId = "unarray";
+          }
+        ];
+        features = {
+          "bit-set" = [ "dep:bit-set" ];
+          "default" = [ "std" "fork" "timeout" "bit-set" "break-dead-code" ];
+          "default-code-coverage" = [ "std" "fork" "timeout" "bit-set" ];
+          "fork" = [ "std" "rusty-fork" "tempfile" ];
+          "hardware-rng" = [ "x86" ];
+          "lazy_static" = [ "dep:lazy_static" ];
+          "quick-error" = [ "dep:quick-error" ];
+          "regex-syntax" = [ "dep:regex-syntax" ];
+          "rusty-fork" = [ "dep:rusty-fork" ];
+          "std" = [ "rand/std" "byteorder/std" "lazy_static" "quick-error" "regex-syntax" "num-traits/std" ];
+          "tempfile" = [ "dep:tempfile" ];
+          "timeout" = [ "fork" "rusty-fork/timeout" ];
+          "x86" = [ "dep:x86" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "bit-set" "break-dead-code" "default" "fork" "lazy_static" "quick-error" "regex-syntax" "rusty-fork" "std" "tempfile" "timeout" ];
+      };
+      "prost" = rec {
+        crateName = "prost";
+        version = "0.11.9";
+        edition = "2021";
+        sha256 = "1kc1hva2h894hc0zf6r4r8fsxfpazf7xn5rj3jya9sbrsyhym0hb";
+        authors = [
+          "Dan Burkert <dan@danburkert.com>"
+          "Lucio Franco <luciofranco14@gmail.com"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "prost-derive";
+            packageId = "prost-derive";
+            optional = true;
+          }
+        ];
+        features = {
+          "default" = [ "prost-derive" "std" ];
+          "prost-derive" = [ "dep:prost-derive" ];
+        };
+        resolvedDefaultFeatures = [ "default" "prost-derive" "std" ];
+      };
+      "prost-build" = rec {
+        crateName = "prost-build";
+        version = "0.11.9";
+        edition = "2021";
+        sha256 = "0w5jx97q96ydhkg67wx3lb11kfy8195c56g0476glzws5iak758i";
+        authors = [
+          "Dan Burkert <dan@danburkert.com>"
+          "Lucio Franco <luciofranco14@gmail.com>"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "heck";
+            packageId = "heck";
+          }
+          {
+            name = "itertools";
+            packageId = "itertools";
+            usesDefaultFeatures = false;
+            features = [ "use_alloc" ];
+          }
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+          }
+          {
+            name = "log";
+            packageId = "log";
+          }
+          {
+            name = "multimap";
+            packageId = "multimap";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "petgraph";
+            packageId = "petgraph";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "prettyplease";
+            packageId = "prettyplease";
+            optional = true;
+          }
+          {
+            name = "prost";
+            packageId = "prost";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "prost-types";
+            packageId = "prost-types";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "regex";
+            packageId = "regex";
+            usesDefaultFeatures = false;
+            features = [ "std" "unicode-bool" ];
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            optional = true;
+            features = [ "full" ];
+          }
+          {
+            name = "tempfile";
+            packageId = "tempfile";
+          }
+          {
+            name = "which";
+            packageId = "which";
+          }
+        ];
+        features = {
+          "cleanup-markdown" = [ "pulldown-cmark" "pulldown-cmark-to-cmark" ];
+          "default" = [ "format" ];
+          "format" = [ "prettyplease" "syn" ];
+          "prettyplease" = [ "dep:prettyplease" ];
+          "pulldown-cmark" = [ "dep:pulldown-cmark" ];
+          "pulldown-cmark-to-cmark" = [ "dep:pulldown-cmark-to-cmark" ];
+          "syn" = [ "dep:syn" ];
+        };
+        resolvedDefaultFeatures = [ "default" "format" "prettyplease" "syn" ];
+      };
+      "prost-derive" = rec {
+        crateName = "prost-derive";
+        version = "0.11.9";
+        edition = "2021";
+        sha256 = "1d3mw2s2jba1f7wcjmjd6ha2a255p2rmynxhm1nysv9w1z8xilp5";
+        procMacro = true;
+        authors = [
+          "Dan Burkert <dan@danburkert.com>"
+          "Lucio Franco <luciofranco14@gmail.com>"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "anyhow";
+            packageId = "anyhow";
+          }
+          {
+            name = "itertools";
+            packageId = "itertools";
+            usesDefaultFeatures = false;
+            features = [ "use_alloc" ];
+          }
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "extra-traits" ];
+          }
+        ];
+
+      };
+      "prost-types" = rec {
+        crateName = "prost-types";
+        version = "0.11.9";
+        edition = "2021";
+        sha256 = "04ryk38sqkp2nf4dgdqdfbgn6zwwvjraw6hqq6d9a6088shj4di1";
+        authors = [
+          "Dan Burkert <dan@danburkert.com>"
+          "Lucio Franco <luciofranco14@gmail.com"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "prost";
+            packageId = "prost";
+            usesDefaultFeatures = false;
+            features = [ "prost-derive" ];
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "std" = [ "prost/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "quick-error 1.2.3" = rec {
+        crateName = "quick-error";
+        version = "1.2.3";
+        edition = "2015";
+        sha256 = "1q6za3v78hsspisc197bg3g7rpc989qycy8ypr8ap8igv10ikl51";
+        authors = [
+          "Paul Colomiets <paul@colomiets.name>"
+          "Colin Kiegel <kiegel@gmx.de>"
+        ];
+
+      };
+      "quick-error 2.0.1" = rec {
+        crateName = "quick-error";
+        version = "2.0.1";
+        edition = "2018";
+        sha256 = "18z6r2rcjvvf8cn92xjhm2qc3jpd1ljvcbf12zv0k9p565gmb4x9";
+        authors = [
+          "Paul Colomiets <paul@colomiets.name>"
+          "Colin Kiegel <kiegel@gmx.de>"
+        ];
+
+      };
+      "quote 0.6.13" = rec {
+        crateName = "quote";
+        version = "0.6.13";
+        edition = "2015";
+        sha256 = "1qgqq48jymp5h4y082aanf25hrw6bpb678xh3zw993qfhxmkpqkc";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 0.4.30";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "proc-macro" ];
+          "proc-macro" = [ "proc-macro2/proc-macro" ];
+        };
+        resolvedDefaultFeatures = [ "default" "proc-macro" ];
+      };
+      "quote 1.0.26" = rec {
+        crateName = "quote";
+        version = "1.0.26";
+        edition = "2018";
+        sha256 = "1z521piwggwzs0rj4wjx4ma6af1g6f1h5dkp382y5akqyx5sy924";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "proc-macro" ];
+          "proc-macro" = [ "proc-macro2/proc-macro" ];
+        };
+        resolvedDefaultFeatures = [ "default" "proc-macro" ];
+      };
+      "radix_trie" = rec {
+        crateName = "radix_trie";
+        version = "0.2.1";
+        edition = "2018";
+        sha256 = "1zaq3im5ss03w91ij11cj97vvzc5y1f3064d9pi2ysnwziww2sf0";
+        authors = [
+          "Michael Sproul <micsproul@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "endian-type";
+            packageId = "endian-type";
+          }
+          {
+            name = "nibble_vec";
+            packageId = "nibble_vec";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "rand 0.4.6" = rec {
+        crateName = "rand";
+        version = "0.4.6";
+        edition = "2015";
+        sha256 = "14qjfv3gggzhnma20k0sc1jf8y6pplsaq7n1j9ls5c8kf2wl0a2m";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "fuchsia-cprng";
+            packageId = "fuchsia-cprng";
+            target = { target, features }: ("fuchsia" == target."os");
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            optional = true;
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.3.1";
+            usesDefaultFeatures = false;
+            target = { target, features }: ("sgx" == target."env");
+          }
+          {
+            name = "rdrand";
+            packageId = "rdrand";
+            target = { target, features }: ("sgx" == target."env");
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "minwindef" "ntsecapi" "profileapi" "winnt" ];
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "libc" = [ "dep:libc" ];
+          "nightly" = [ "i128_support" ];
+          "std" = [ "libc" ];
+        };
+        resolvedDefaultFeatures = [ "default" "libc" "std" ];
+      };
+      "rand 0.8.5" = rec {
+        crateName = "rand";
+        version = "0.8.5";
+        edition = "2018";
+        sha256 = "013l6931nn7gkc23jz5mm3qdhf93jjf0fg64nz2lp4i51qd8vbrl";
+        authors = [
+          "The Rand Project Developers"
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            optional = true;
+            usesDefaultFeatures = false;
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "rand_chacha";
+            packageId = "rand_chacha";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.6.4";
+          }
+        ];
+        features = {
+          "alloc" = [ "rand_core/alloc" ];
+          "default" = [ "std" "std_rng" ];
+          "getrandom" = [ "rand_core/getrandom" ];
+          "libc" = [ "dep:libc" ];
+          "log" = [ "dep:log" ];
+          "packed_simd" = [ "dep:packed_simd" ];
+          "rand_chacha" = [ "dep:rand_chacha" ];
+          "serde" = [ "dep:serde" ];
+          "serde1" = [ "serde" "rand_core/serde1" ];
+          "simd_support" = [ "packed_simd" ];
+          "std" = [ "rand_core/std" "rand_chacha/std" "alloc" "getrandom" "libc" ];
+          "std_rng" = [ "rand_chacha" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "getrandom" "libc" "rand_chacha" "small_rng" "std" "std_rng" ];
+      };
+      "rand_chacha" = rec {
+        crateName = "rand_chacha";
+        version = "0.3.1";
+        edition = "2018";
+        sha256 = "123x2adin558xbhvqb8w4f6syjsdkmqff8cxwhmjacpsl1ihmhg6";
+        authors = [
+          "The Rand Project Developers"
+          "The Rust Project Developers"
+          "The CryptoCorrosion Contributors"
+        ];
+        dependencies = [
+          {
+            name = "ppv-lite86";
+            packageId = "ppv-lite86";
+            usesDefaultFeatures = false;
+            features = [ "simd" ];
+          }
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.6.4";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+          "serde1" = [ "serde" ];
+          "std" = [ "ppv-lite86/std" ];
+        };
+        resolvedDefaultFeatures = [ "std" ];
+      };
+      "rand_core 0.3.1" = rec {
+        crateName = "rand_core";
+        version = "0.3.1";
+        edition = "2015";
+        sha256 = "0jzdgszfa4bliigiy4hi66k7fs3gfwi2qxn8vik84ph77fwdwvvs";
+        authors = [
+          "The Rand Project Developers"
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.4.2";
+          }
+        ];
+        features = {
+          "alloc" = [ "rand_core/alloc" ];
+          "default" = [ "std" ];
+          "serde1" = [ "rand_core/serde1" ];
+          "std" = [ "rand_core/std" ];
+        };
+      };
+      "rand_core 0.4.2" = rec {
+        crateName = "rand_core";
+        version = "0.4.2";
+        edition = "2015";
+        sha256 = "1p09ynysrq1vcdlmcqnapq4qakl2yd1ng3kxh3qscpx09k2a6cww";
+        authors = [
+          "The Rand Project Developers"
+          "The Rust Project Developers"
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+          "serde1" = [ "serde" "serde_derive" ];
+          "serde_derive" = [ "dep:serde_derive" ];
+          "std" = [ "alloc" ];
+        };
+      };
+      "rand_core 0.6.4" = rec {
+        crateName = "rand_core";
+        version = "0.6.4";
+        edition = "2018";
+        sha256 = "0b4j2v4cb5krak1pv6kakv4sz6xcwbrmy2zckc32hsigbrwy82zc";
+        authors = [
+          "The Rand Project Developers"
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "getrandom";
+            packageId = "getrandom";
+            optional = true;
+          }
+        ];
+        features = {
+          "getrandom" = [ "dep:getrandom" ];
+          "serde" = [ "dep:serde" ];
+          "serde1" = [ "serde" ];
+          "std" = [ "alloc" "getrandom" "getrandom/std" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "getrandom" "std" ];
+      };
+      "rand_xorshift" = rec {
+        crateName = "rand_xorshift";
+        version = "0.3.0";
+        edition = "2018";
+        sha256 = "13vcag7gmqspzyabfl1gr9ykvxd2142q2agrj8dkyjmfqmgg4nyj";
+        authors = [
+          "The Rand Project Developers"
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.6.4";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+          "serde1" = [ "serde" ];
+        };
+      };
+      "rand_xoshiro" = rec {
+        crateName = "rand_xoshiro";
+        version = "0.6.0";
+        edition = "2018";
+        sha256 = "1ajsic84rzwz5qr0mzlay8vi17swqi684bqvwqyiim3flfrcv5vg";
+        authors = [
+          "The Rand Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.6.4";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+          "serde1" = [ "serde" ];
+        };
+      };
+      "rayon" = rec {
+        crateName = "rayon";
+        version = "1.7.0";
+        edition = "2021";
+        sha256 = "0fzh8w5ds1qjhilll4rkpd3kimw70zi5605wprxcig1pdqczab8x";
+        authors = [
+          "Niko Matsakis <niko@alum.mit.edu>"
+          "Josh Stone <cuviper@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "either";
+            packageId = "either";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "rayon-core";
+            packageId = "rayon-core";
+          }
+        ];
+
+      };
+      "rayon-core" = rec {
+        crateName = "rayon-core";
+        version = "1.11.0";
+        edition = "2021";
+        sha256 = "13dymrhhdilzpbfh3aylv6ariayqdfk614b3frvwixb6d6yrb3sb";
+        authors = [
+          "Niko Matsakis <niko@alum.mit.edu>"
+          "Josh Stone <cuviper@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "crossbeam-channel";
+            packageId = "crossbeam-channel";
+          }
+          {
+            name = "crossbeam-deque";
+            packageId = "crossbeam-deque";
+          }
+          {
+            name = "crossbeam-utils";
+            packageId = "crossbeam-utils";
+          }
+          {
+            name = "num_cpus";
+            packageId = "num_cpus";
+          }
+        ];
+
+      };
+      "rdrand" = rec {
+        crateName = "rdrand";
+        version = "0.4.0";
+        edition = "2015";
+        sha256 = "1cjq0kwx1bk7jx3kzyciiish5gqsj7620dm43dc52sr8fzmm9037";
+        authors = [
+          "Simonas Kazlauskas <rdrand@kazlauskas.me>"
+        ];
+        dependencies = [
+          {
+            name = "rand_core";
+            packageId = "rand_core 0.3.1";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "redox_syscall 0.2.16" = rec {
+        crateName = "redox_syscall";
+        version = "0.2.16";
+        edition = "2018";
+        sha256 = "16jicm96kjyzm802cxdd1k9jmcph0db1a4lhslcnhjsvhp0mhnpv";
+        libName = "syscall";
+        authors = [
+          "Jeremy Soller <jackpot51@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+        ];
+
+      };
+      "redox_syscall 0.3.5" = rec {
+        crateName = "redox_syscall";
+        version = "0.3.5";
+        edition = "2018";
+        sha256 = "0acgiy2lc1m2vr8cr33l5s7k9wzby8dybyab1a9p753hcbr68xjn";
+        libName = "syscall";
+        authors = [
+          "Jeremy Soller <jackpot51@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+        ];
+        features = {
+          "core" = [ "dep:core" ];
+          "rustc-dep-of-std" = [ "core" "bitflags/rustc-dep-of-std" ];
+        };
+      };
+      "redox_users" = rec {
+        crateName = "redox_users";
+        version = "0.4.3";
+        edition = "2018";
+        sha256 = "0asw3s4iy69knafkhvlbchy230qawc297vddjdwjs5nglwvxhcxh";
+        authors = [
+          "Jose Narvaez <goyox86@gmail.com>"
+          "Wesley Hershberger <mggmugginsmc@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "getrandom";
+            packageId = "getrandom";
+            features = [ "std" ];
+          }
+          {
+            name = "redox_syscall";
+            packageId = "redox_syscall 0.2.16";
+          }
+          {
+            name = "thiserror";
+            packageId = "thiserror";
+          }
+        ];
+        features = {
+          "auth" = [ "rust-argon2" "zeroize" ];
+          "default" = [ "auth" ];
+          "rust-argon2" = [ "dep:rust-argon2" ];
+          "zeroize" = [ "dep:zeroize" ];
+        };
+      };
+      "regex" = rec {
+        crateName = "regex";
+        version = "1.8.1";
+        edition = "2021";
+        sha256 = "0w2kgdvs2fsv39hrsb912zjq3bx5vw1cchslvbi6mk1iycbyd0xg";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "aho-corasick";
+            packageId = "aho-corasick";
+            optional = true;
+          }
+          {
+            name = "memchr";
+            packageId = "memchr";
+            optional = true;
+          }
+          {
+            name = "regex-syntax";
+            packageId = "regex-syntax 0.7.1";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "aho-corasick" = [ "dep:aho-corasick" ];
+          "default" = [ "std" "perf" "unicode" "regex-syntax/default" ];
+          "memchr" = [ "dep:memchr" ];
+          "perf" = [ "perf-cache" "perf-dfa" "perf-inline" "perf-literal" ];
+          "perf-literal" = [ "aho-corasick" "memchr" ];
+          "unicode" = [ "unicode-age" "unicode-bool" "unicode-case" "unicode-gencat" "unicode-perl" "unicode-script" "unicode-segment" "regex-syntax/unicode" ];
+          "unicode-age" = [ "regex-syntax/unicode-age" ];
+          "unicode-bool" = [ "regex-syntax/unicode-bool" ];
+          "unicode-case" = [ "regex-syntax/unicode-case" ];
+          "unicode-gencat" = [ "regex-syntax/unicode-gencat" ];
+          "unicode-perl" = [ "regex-syntax/unicode-perl" ];
+          "unicode-script" = [ "regex-syntax/unicode-script" ];
+          "unicode-segment" = [ "regex-syntax/unicode-segment" ];
+          "unstable" = [ "pattern" ];
+          "use_std" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "aho-corasick" "default" "memchr" "perf" "perf-cache" "perf-dfa" "perf-inline" "perf-literal" "std" "unicode" "unicode-age" "unicode-bool" "unicode-case" "unicode-gencat" "unicode-perl" "unicode-script" "unicode-segment" ];
+      };
+      "regex-syntax 0.6.29" = rec {
+        crateName = "regex-syntax";
+        version = "0.6.29";
+        edition = "2018";
+        sha256 = "1qgj49vm6y3zn1hi09x91jvgkl2b1fiaq402skj83280ggfwcqpi";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        features = {
+          "default" = [ "unicode" ];
+          "unicode" = [ "unicode-age" "unicode-bool" "unicode-case" "unicode-gencat" "unicode-perl" "unicode-script" "unicode-segment" ];
+        };
+        resolvedDefaultFeatures = [ "default" "unicode" "unicode-age" "unicode-bool" "unicode-case" "unicode-gencat" "unicode-perl" "unicode-script" "unicode-segment" ];
+      };
+      "regex-syntax 0.7.1" = rec {
+        crateName = "regex-syntax";
+        version = "0.7.1";
+        edition = "2021";
+        sha256 = "0g1s6ra0ra8xy1fxscspd406c3pn53bjm1is8phamlwvy6a656d5";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        features = {
+          "default" = [ "std" "unicode" ];
+          "unicode" = [ "unicode-age" "unicode-bool" "unicode-case" "unicode-gencat" "unicode-perl" "unicode-script" "unicode-segment" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" "unicode" "unicode-age" "unicode-bool" "unicode-case" "unicode-gencat" "unicode-perl" "unicode-script" "unicode-segment" ];
+      };
+      "remove_dir_all" = rec {
+        crateName = "remove_dir_all";
+        version = "0.5.3";
+        edition = "2015";
+        sha256 = "1rzqbsgkmr053bxxl04vmvsd1njyz0nxvly97aip6aa2cmb15k9s";
+        authors = [
+          "Aaronepower <theaaronepower@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "std" "errhandlingapi" "winerror" "fileapi" "winbase" ];
+          }
+        ];
+
+      };
+      "rnix" = rec {
+        crateName = "rnix";
+        version = "0.11.0";
+        edition = "2021";
+        sha256 = "0pybq9gp4b7lp0066236jpqi9lgb1bzvqc9axymwrq3hxgdwwddv";
+        authors = [
+          "jD91mZM2 <me@krake.one>"
+        ];
+        dependencies = [
+          {
+            name = "rowan";
+            packageId = "rowan";
+          }
+        ];
+
+      };
+      "rowan" = rec {
+        crateName = "rowan";
+        version = "0.15.11";
+        edition = "2021";
+        sha256 = "1gvqa02nmssbl10a4apvi63l95pfllnhxqvasmg4ffj8z7z9qi34";
+        authors = [
+          "Aleksey Kladov <aleksey.kladov@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "countme";
+            packageId = "countme";
+          }
+          {
+            name = "hashbrown";
+            packageId = "hashbrown";
+            usesDefaultFeatures = false;
+            features = [ "inline-more" ];
+          }
+          {
+            name = "memoffset";
+            packageId = "memoffset";
+          }
+          {
+            name = "rustc-hash";
+            packageId = "rustc-hash";
+          }
+          {
+            name = "text-size";
+            packageId = "text-size";
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+          "serde1" = [ "serde" "text-size/serde" ];
+        };
+      };
+      "rustc-hash" = rec {
+        crateName = "rustc-hash";
+        version = "1.1.0";
+        edition = "2015";
+        sha256 = "1qkc5khrmv5pqi5l5ca9p5nl5hs742cagrndhbrlk3dhlrx3zm08";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        features = {
+          "default" = [ "std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "rustix" = rec {
+        crateName = "rustix";
+        version = "0.37.19";
+        edition = "2018";
+        sha256 = "0gb12rp992bh2h5msqcbpdsx6h1gslsb0zpp5hdnyxj2hnfp5y5c";
+        authors = [
+          "Dan Gohman <dev@sunfishcode.online>"
+          "Jakub Konka <kubkon@jakubkonka.com>"
+        ];
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "errno";
+            packageId = "errno";
+            rename = "libc_errno";
+            optional = true;
+            usesDefaultFeatures = false;
+            target = { target, features }: ((!(target."rustix_use_libc" or false)) && (!(target."miri" or false)) && ("linux" == target."os") && (("x86" == target."arch") || (("x86_64" == target."arch") && ("64" == target."pointer_width")) || (("little" == target."endian") && (("arm" == target."arch") || (("aarch64" == target."arch") && ("64" == target."pointer_width")) || ("powerpc64" == target."arch") || ("riscv64" == target."arch") || ("mips" == target."arch") || ("mips64" == target."arch")))));
+          }
+          {
+            name = "errno";
+            packageId = "errno";
+            rename = "libc_errno";
+            usesDefaultFeatures = false;
+            target = { target, features }: ((!(target."windows" or false)) && ((target."rustix_use_libc" or false) || (target."miri" or false) || (!(("linux" == target."os") && (("x86" == target."arch") || (("x86_64" == target."arch") && ("64" == target."pointer_width")) || (("little" == target."endian") && (("arm" == target."arch") || (("aarch64" == target."arch") && ("64" == target."pointer_width")) || ("powerpc64" == target."arch") || ("riscv64" == target."arch") || ("mips" == target."arch") || ("mips64" == target."arch"))))))));
+          }
+          {
+            name = "errno";
+            packageId = "errno";
+            rename = "libc_errno";
+            usesDefaultFeatures = false;
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "io-lifetimes";
+            packageId = "io-lifetimes";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "close" ];
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            optional = true;
+            target = { target, features }: ((!(target."rustix_use_libc" or false)) && (!(target."miri" or false)) && ("linux" == target."os") && (("x86" == target."arch") || (("x86_64" == target."arch") && ("64" == target."pointer_width")) || (("little" == target."endian") && (("arm" == target."arch") || (("aarch64" == target."arch") && ("64" == target."pointer_width")) || ("powerpc64" == target."arch") || ("riscv64" == target."arch") || ("mips" == target."arch") || ("mips64" == target."arch")))));
+            features = [ "extra_traits" ];
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: ((!(target."windows" or false)) && ((target."rustix_use_libc" or false) || (target."miri" or false) || (!(("linux" == target."os") && (("x86" == target."arch") || (("x86_64" == target."arch") && ("64" == target."pointer_width")) || (("little" == target."endian") && (("arm" == target."arch") || (("aarch64" == target."arch") && ("64" == target."pointer_width")) || ("powerpc64" == target."arch") || ("riscv64" == target."arch") || ("mips" == target."arch") || ("mips64" == target."arch"))))))));
+            features = [ "extra_traits" ];
+          }
+          {
+            name = "linux-raw-sys";
+            packageId = "linux-raw-sys";
+            usesDefaultFeatures = false;
+            target = { target, features }: ((("android" == target."os") || ("linux" == target."os")) && ((target."rustix_use_libc" or false) || (target."miri" or false) || (!(("linux" == target."os") && (("x86" == target."arch") || (("x86_64" == target."arch") && ("64" == target."pointer_width")) || (("little" == target."endian") && (("arm" == target."arch") || (("aarch64" == target."arch") && ("64" == target."pointer_width")) || ("powerpc64" == target."arch") || ("riscv64" == target."arch") || ("mips" == target."arch") || ("mips64" == target."arch"))))))));
+            features = [ "general" "ioctl" "no_std" ];
+          }
+          {
+            name = "linux-raw-sys";
+            packageId = "linux-raw-sys";
+            usesDefaultFeatures = false;
+            target = { target, features }: ((!(target."rustix_use_libc" or false)) && (!(target."miri" or false)) && ("linux" == target."os") && (("x86" == target."arch") || (("x86_64" == target."arch") && ("64" == target."pointer_width")) || (("little" == target."endian") && (("arm" == target."arch") || (("aarch64" == target."arch") && ("64" == target."pointer_width")) || ("powerpc64" == target."arch") || ("riscv64" == target."arch") || ("mips" == target."arch") || ("mips64" == target."arch")))));
+            features = [ "general" "errno" "ioctl" "no_std" ];
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_Foundation" "Win32_Networking_WinSock" "Win32_NetworkManagement_IpHelper" "Win32_System_Threading" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "errno";
+            packageId = "errno";
+            rename = "libc_errno";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "io-lifetimes";
+            packageId = "io-lifetimes";
+            usesDefaultFeatures = false;
+            features = [ "close" ];
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+        ];
+        features = {
+          "all-apis" = [ "fs" "io_uring" "mm" "net" "param" "process" "procfs" "rand" "runtime" "termios" "thread" "time" ];
+          "all-impls" = [ "os_pipe" "fs-err" ];
+          "alloc" = [ "dep:alloc" ];
+          "cc" = [ "dep:cc" ];
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "default" = [ "std" "use-libc-auxv" ];
+          "fs-err" = [ "io-lifetimes/fs-err" ];
+          "io-lifetimes" = [ "dep:io-lifetimes" ];
+          "io_uring" = [ "fs" "net" ];
+          "itoa" = [ "dep:itoa" ];
+          "libc" = [ "dep:libc" ];
+          "libc_errno" = [ "dep:libc_errno" ];
+          "linux_latest" = [ "linux_4_11" ];
+          "once_cell" = [ "dep:once_cell" ];
+          "os_pipe" = [ "io-lifetimes/os_pipe" ];
+          "param" = [ "fs" ];
+          "procfs" = [ "once_cell" "itoa" "fs" ];
+          "rustc-dep-of-std" = [ "core" "alloc" "compiler_builtins" "linux-raw-sys/rustc-dep-of-std" "bitflags/rustc-dep-of-std" ];
+          "std" = [ "io-lifetimes" ];
+          "use-libc" = [ "libc_errno" "libc" ];
+          "use-libc-auxv" = [ "libc" ];
+        };
+        resolvedDefaultFeatures = [ "default" "fs" "io-lifetimes" "libc" "std" "termios" "use-libc-auxv" ];
+      };
+      "rustversion" = rec {
+        crateName = "rustversion";
+        version = "1.0.12";
+        edition = "2018";
+        sha256 = "01mzns4b7vfcxsyf63ck68gachqcbqzsfs6iwzrv6j449p70hcjg";
+        procMacro = true;
+        build = "build/build.rs";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+
+      };
+      "rusty-fork" = rec {
+        crateName = "rusty-fork";
+        version = "0.3.0";
+        edition = "2018";
+        sha256 = "0kxwq5c480gg6q0j3bg4zzyfh2kwmc3v2ba94jw8ncjc8mpcqgfb";
+        authors = [
+          "Jason Lingle"
+        ];
+        dependencies = [
+          {
+            name = "fnv";
+            packageId = "fnv";
+          }
+          {
+            name = "quick-error";
+            packageId = "quick-error 1.2.3";
+          }
+          {
+            name = "tempfile";
+            packageId = "tempfile";
+          }
+          {
+            name = "wait-timeout";
+            packageId = "wait-timeout";
+            optional = true;
+          }
+        ];
+        features = {
+          "default" = [ "timeout" ];
+          "timeout" = [ "wait-timeout" ];
+          "wait-timeout" = [ "dep:wait-timeout" ];
+        };
+        resolvedDefaultFeatures = [ "timeout" "wait-timeout" ];
+      };
+      "rustyline" = rec {
+        crateName = "rustyline";
+        version = "10.1.1";
+        edition = "2018";
+        sha256 = "1vvsd68cch0lpcg6mcwfvfdd6r4cxbwis3bf9443phzkqcr3rs61";
+        authors = [
+          "Katsu Kawakami <kkawa1570@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "bitflags";
+            packageId = "bitflags";
+          }
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "clipboard-win";
+            packageId = "clipboard-win";
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "dirs-next";
+            packageId = "dirs-next";
+            optional = true;
+          }
+          {
+            name = "fd-lock";
+            packageId = "fd-lock";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+          {
+            name = "log";
+            packageId = "log";
+          }
+          {
+            name = "memchr";
+            packageId = "memchr";
+          }
+          {
+            name = "nix";
+            packageId = "nix";
+            usesDefaultFeatures = false;
+            target = { target, features }: (target."unix" or false);
+            features = [ "fs" "ioctl" "poll" "signal" "term" ];
+          }
+          {
+            name = "radix_trie";
+            packageId = "radix_trie";
+            optional = true;
+          }
+          {
+            name = "scopeguard";
+            packageId = "scopeguard";
+            target = { target, features }: (target."windows" or false);
+          }
+          {
+            name = "unicode-segmentation";
+            packageId = "unicode-segmentation";
+          }
+          {
+            name = "unicode-width";
+            packageId = "unicode-width";
+          }
+          {
+            name = "utf8parse";
+            packageId = "utf8parse";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "consoleapi" "handleapi" "synchapi" "minwindef" "processenv" "std" "winbase" "wincon" "winuser" ];
+          }
+        ];
+        features = {
+          "case_insensitive_history_search" = [ "regex" ];
+          "custom-bindings" = [ "radix_trie" ];
+          "default" = [ "custom-bindings" "with-dirs" ];
+          "dirs-next" = [ "dep:dirs-next" ];
+          "radix_trie" = [ "dep:radix_trie" ];
+          "regex" = [ "dep:regex" ];
+          "signal-hook" = [ "dep:signal-hook" ];
+          "skim" = [ "dep:skim" ];
+          "with-dirs" = [ "dirs-next" ];
+          "with-fuzzy" = [ "skim" ];
+        };
+        resolvedDefaultFeatures = [ "custom-bindings" "default" "dirs-next" "radix_trie" "with-dirs" ];
+      };
+      "ryu" = rec {
+        crateName = "ryu";
+        version = "1.0.13";
+        edition = "2018";
+        sha256 = "0hchlxvjmsz51l06c7r8zwj45pm8bhc3x3czcih27rkx8v03j4zr";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        features = {
+          "no-panic" = [ "dep:no-panic" ];
+        };
+      };
+      "same-file" = rec {
+        crateName = "same-file";
+        version = "1.0.6";
+        edition = "2018";
+        sha256 = "00h5j1w87dmhnvbv9l8bic3y7xxsnjmssvifw2ayvgx9mb1ivz4k";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "winapi-util";
+            packageId = "winapi-util";
+            target = { target, features }: (target."windows" or false);
+          }
+        ];
+
+      };
+      "scopeguard" = rec {
+        crateName = "scopeguard";
+        version = "1.1.0";
+        edition = "2015";
+        sha256 = "1kbqm85v43rq92vx7hfiay6pmcga03vrjbbfwqpyj3pwsg3b16nj";
+        authors = [
+          "bluss"
+        ];
+        features = {
+          "default" = [ "use_std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "use_std" ];
+      };
+      "serde" = rec {
+        crateName = "serde";
+        version = "1.0.162";
+        edition = "2015";
+        sha256 = "1dksgs0zi9wdh3bm3gzzsvmgg39fn8vb4d8gbz09haswmghzdcki";
+        authors = [
+          "Erick Tryzelaar <erick.tryzelaar@gmail.com>"
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "serde_derive";
+            packageId = "serde_derive";
+            optional = true;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "serde_derive";
+            packageId = "serde_derive";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "derive" = [ "serde_derive" ];
+          "serde_derive" = [ "dep:serde_derive" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "default" "derive" "rc" "serde_derive" "std" ];
+      };
+      "serde_derive" = rec {
+        crateName = "serde_derive";
+        version = "1.0.162";
+        edition = "2015";
+        sha256 = "1diwx4c86b63mgmzbd5nvj8imjwhipm48jlhi62bar7xa91q3852";
+        procMacro = true;
+        authors = [
+          "Erick Tryzelaar <erick.tryzelaar@gmail.com>"
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+          }
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "serde_json" = rec {
+        crateName = "serde_json";
+        version = "1.0.96";
+        edition = "2018";
+        sha256 = "1waj3qwpa610vmksnzcmkll6vaw7nf7v3ckj4v0wlfs0a153jz85";
+        authors = [
+          "Erick Tryzelaar <erick.tryzelaar@gmail.com>"
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "itoa";
+            packageId = "itoa";
+          }
+          {
+            name = "ryu";
+            packageId = "ryu";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            usesDefaultFeatures = false;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "derive" ];
+          }
+        ];
+        features = {
+          "alloc" = [ "serde/alloc" ];
+          "default" = [ "std" ];
+          "indexmap" = [ "dep:indexmap" ];
+          "preserve_order" = [ "indexmap" "std" ];
+          "std" = [ "serde/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "serde_spanned" = rec {
+        crateName = "serde_spanned";
+        version = "0.6.1";
+        edition = "2021";
+        sha256 = "1x7wqhqay4bgkaq8dmvb9z01mk2z0j0g9jl5nb9ynv3aanpqrz8f";
+        dependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+            optional = true;
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "serde" ];
+      };
+      "sha-1" = rec {
+        crateName = "sha-1";
+        version = "0.8.2";
+        edition = "2015";
+        sha256 = "1pv387q0r7llk2cqzyq0nivzvkgqgzsiygqzlv7b68z9xl5lvngp";
+        libName = "sha1";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "block-buffer";
+            packageId = "block-buffer 0.7.3";
+          }
+          {
+            name = "digest";
+            packageId = "digest 0.8.1";
+          }
+          {
+            name = "fake-simd";
+            packageId = "fake-simd";
+          }
+          {
+            name = "opaque-debug";
+            packageId = "opaque-debug";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "digest";
+            packageId = "digest 0.8.1";
+            features = [ "dev" ];
+          }
+        ];
+        features = {
+          "asm" = [ "sha1-asm" ];
+          "asm-aarch64" = [ "asm" "libc" ];
+          "default" = [ "std" ];
+          "libc" = [ "dep:libc" ];
+          "sha1-asm" = [ "dep:sha1-asm" ];
+          "std" = [ "digest/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "sha2 0.10.6" = rec {
+        crateName = "sha2";
+        version = "0.10.6";
+        edition = "2018";
+        sha256 = "1h5xrrv2y06kr1gsz4pwrm3lsp206nm2gjxgbf21wfrfzsavgrl2";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "cpufeatures";
+            packageId = "cpufeatures";
+            target = { target, features }: (("aarch64" == target."arch") || ("x86_64" == target."arch") || ("x86" == target."arch"));
+          }
+          {
+            name = "digest";
+            packageId = "digest 0.10.6";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "digest";
+            packageId = "digest 0.10.6";
+            features = [ "dev" ];
+          }
+        ];
+        features = {
+          "asm" = [ "sha2-asm" ];
+          "asm-aarch64" = [ "asm" ];
+          "default" = [ "std" ];
+          "oid" = [ "digest/oid" ];
+          "sha2-asm" = [ "dep:sha2-asm" ];
+          "std" = [ "digest/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "sha2 0.8.2" = rec {
+        crateName = "sha2";
+        version = "0.8.2";
+        edition = "2015";
+        sha256 = "0s9yddvyg6anaikdl86wmwfim25c0d4m0xq0y2ghs34alxpg8mm2";
+        authors = [
+          "RustCrypto Developers"
+        ];
+        dependencies = [
+          {
+            name = "block-buffer";
+            packageId = "block-buffer 0.7.3";
+          }
+          {
+            name = "digest";
+            packageId = "digest 0.8.1";
+          }
+          {
+            name = "fake-simd";
+            packageId = "fake-simd";
+          }
+          {
+            name = "opaque-debug";
+            packageId = "opaque-debug";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "digest";
+            packageId = "digest 0.8.1";
+            features = [ "dev" ];
+          }
+        ];
+        features = {
+          "asm" = [ "sha2-asm" ];
+          "asm-aarch64" = [ "asm" "libc" ];
+          "default" = [ "std" ];
+          "libc" = [ "dep:libc" ];
+          "sha2-asm" = [ "dep:sha2-asm" ];
+          "std" = [ "digest/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "sharded-slab" = rec {
+        crateName = "sharded-slab";
+        version = "0.1.4";
+        edition = "2018";
+        sha256 = "0cbb8kgwsyr3zzhsv8jrs3y1j3vsw4jxil42lfq31ikhdy0bl3wh";
+        authors = [
+          "Eliza Weisman <eliza@buoyant.io>"
+        ];
+        dependencies = [
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+          }
+        ];
+        features = {
+          "loom" = [ "dep:loom" ];
+        };
+      };
+      "slab" = rec {
+        crateName = "slab";
+        version = "0.4.8";
+        edition = "2018";
+        sha256 = "0bgwxig8gkqp6gz8rvrpdj6qwa10karnsxwx7wsj5ay8kcf3aa35";
+        authors = [
+          "Carl Lerche <me@carllerche.com>"
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        features = {
+          "default" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "sled" = rec {
+        crateName = "sled";
+        version = "0.34.7";
+        edition = "2018";
+        sha256 = "0dcr2s7cylj5mb33ci3kpx7fz797jwvysnl5airrir9cgirv95kz";
+        authors = [
+          "Tyler Neely <t@jujit.su>"
+        ];
+        dependencies = [
+          {
+            name = "crc32fast";
+            packageId = "crc32fast";
+          }
+          {
+            name = "crossbeam-epoch";
+            packageId = "crossbeam-epoch";
+          }
+          {
+            name = "crossbeam-utils";
+            packageId = "crossbeam-utils";
+          }
+          {
+            name = "fs2";
+            packageId = "fs2";
+            target = { target, features }: (("linux" == target."os") || ("macos" == target."os") || ("windows" == target."os"));
+          }
+          {
+            name = "fxhash";
+            packageId = "fxhash";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+          {
+            name = "log";
+            packageId = "log";
+          }
+          {
+            name = "parking_lot";
+            packageId = "parking_lot";
+          }
+          {
+            name = "zstd";
+            packageId = "zstd";
+            optional = true;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "log";
+            packageId = "log";
+          }
+        ];
+        features = {
+          "backtrace" = [ "dep:backtrace" ];
+          "color-backtrace" = [ "dep:color-backtrace" ];
+          "compression" = [ "zstd" ];
+          "default" = [ "no_metrics" ];
+          "io_uring" = [ "rio" ];
+          "no_logs" = [ "log/max_level_off" ];
+          "pretty_backtrace" = [ "color-backtrace" ];
+          "rio" = [ "dep:rio" ];
+          "testing" = [ "event_log" "lock_free_delays" "compression" "failpoints" "backtrace" ];
+          "zstd" = [ "dep:zstd" ];
+        };
+        resolvedDefaultFeatures = [ "compression" "default" "no_metrics" "zstd" ];
+      };
+      "smallvec" = rec {
+        crateName = "smallvec";
+        version = "1.10.0";
+        edition = "2018";
+        sha256 = "1q2k15fzxgwjpcdv3f323w24rbbfyv711ayz85ila12lg7zbw1x5";
+        authors = [
+          "The Servo Project Developers"
+        ];
+        features = {
+          "arbitrary" = [ "dep:arbitrary" ];
+          "const_new" = [ "const_generics" ];
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "smol_str" = rec {
+        crateName = "smol_str";
+        version = "0.2.0";
+        edition = "2018";
+        sha256 = "1779hpx5ipbcvkdj5zw8zqk3ynn160qvls1gkcr54hwsprmjw8bl";
+        authors = [
+          "Aleksey Kladov <aleksey.kladov@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "derive" ];
+          }
+        ];
+        features = {
+          "arbitrary" = [ "dep:arbitrary" ];
+          "default" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+          "std" = [ "serde?/std" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "socket2" = rec {
+        crateName = "socket2";
+        version = "0.4.9";
+        edition = "2018";
+        sha256 = "0qnn1r41jqj20m0a2nzzjgzndlmpg5maiyjchccaypfqxq8sk934";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+          "Thomas de Zeeuw <thomasdezeeuw@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "handleapi" "ws2ipdef" "ws2tcpip" ];
+          }
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "all" ];
+      };
+      "ssri" = rec {
+        crateName = "ssri";
+        version = "7.0.0";
+        edition = "2018";
+        sha256 = "1wi3yk801a0bgkd51ly83dxzjfq5726hwq5asxwvx7zki39w1km9";
+        authors = [
+          "Kat Marchán <kzm@zkat.tech>"
+        ];
+        dependencies = [
+          {
+            name = "base64";
+            packageId = "base64 0.10.1";
+          }
+          {
+            name = "digest";
+            packageId = "digest 0.8.1";
+          }
+          {
+            name = "hex";
+            packageId = "hex";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            optional = true;
+          }
+          {
+            name = "sha-1";
+            packageId = "sha-1";
+          }
+          {
+            name = "sha2";
+            packageId = "sha2 0.8.2";
+          }
+          {
+            name = "thiserror";
+            packageId = "thiserror";
+          }
+        ];
+        features = {
+          "default" = [ "serde" ];
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "default" "serde" ];
+      };
+      "static_assertions" = rec {
+        crateName = "static_assertions";
+        version = "1.1.0";
+        edition = "2015";
+        sha256 = "0gsl6xmw10gvn3zs1rv99laj5ig7ylffnh71f9l34js4nr4r7sx2";
+        authors = [
+          "Nikolai Vazquez"
+        ];
+        features = { };
+      };
+      "str-buf" = rec {
+        crateName = "str-buf";
+        version = "1.0.6";
+        edition = "2018";
+        sha256 = "1l7q4nha7wpsr0970bfqm773vhmpwr9l6rr8r4gwgrh46wvdh24y";
+        authors = [
+          "Douman <douman@gmx.se>"
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "strsim" = rec {
+        crateName = "strsim";
+        version = "0.10.0";
+        edition = "2015";
+        sha256 = "08s69r4rcrahwnickvi0kq49z524ci50capybln83mg6b473qivk";
+        authors = [
+          "Danny Guo <danny@dannyguo.com>"
+        ];
+
+      };
+      "structmeta" = rec {
+        crateName = "structmeta";
+        version = "0.1.6";
+        edition = "2021";
+        sha256 = "0alyl12b7fab8izrpliil73sxs1ivr5vm0pisallmxlb4zb44j0h";
+        authors = [
+          "frozenlib"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "structmeta-derive";
+            packageId = "structmeta-derive";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "extra-traits" "full" ];
+          }
+        ];
+
+      };
+      "structmeta-derive" = rec {
+        crateName = "structmeta-derive";
+        version = "0.1.6";
+        edition = "2021";
+        sha256 = "14vxik2m3dm7bwx016qfz062fwznkbq02fyq8vby545m0pj0nhi4";
+        procMacro = true;
+        authors = [
+          "frozenlib"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "extra-traits" ];
+          }
+        ];
+
+      };
+      "subtle" = rec {
+        crateName = "subtle";
+        version = "2.4.1";
+        edition = "2015";
+        sha256 = "00b6jzh9gzb0h9n25g06nqr90z3xzqppfhhb260s1hjhh4pg7pkb";
+        authors = [
+          "Isis Lovecruft <isis@patternsinthevoid.net>"
+          "Henry de Valence <hdevalence@hdevalence.ca>"
+        ];
+        features = {
+          "default" = [ "std" "i128" ];
+        };
+      };
+      "syn 0.15.44" = rec {
+        crateName = "syn";
+        version = "0.15.44";
+        edition = "2015";
+        sha256 = "1id5g6x6zihv3j7hwrw3m1jp636bg8dpi671r7zy3jvpkavb794w";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 0.4.30";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "quote";
+            packageId = "quote 0.6.13";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "unicode-xid";
+            packageId = "unicode-xid";
+          }
+        ];
+        features = {
+          "default" = [ "derive" "parsing" "printing" "clone-impls" "proc-macro" ];
+          "printing" = [ "quote" ];
+          "proc-macro" = [ "proc-macro2/proc-macro" "quote/proc-macro" ];
+          "quote" = [ "dep:quote" ];
+        };
+        resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "full" "parsing" "printing" "proc-macro" "quote" ];
+      };
+      "syn 1.0.109" = rec {
+        crateName = "syn";
+        version = "1.0.109";
+        edition = "2018";
+        sha256 = "0ds2if4600bd59wsv7jjgfkayfzy3hnazs394kz6zdkmna8l3dkj";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "unicode-ident";
+            packageId = "unicode-ident";
+          }
+        ];
+        features = {
+          "default" = [ "derive" "parsing" "printing" "clone-impls" "proc-macro" ];
+          "printing" = [ "quote" ];
+          "proc-macro" = [ "proc-macro2/proc-macro" "quote/proc-macro" ];
+          "quote" = [ "dep:quote" ];
+          "test" = [ "syn-test-suite/all-features" ];
+        };
+        resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "extra-traits" "full" "parsing" "printing" "proc-macro" "quote" "visit" "visit-mut" ];
+      };
+      "syn 2.0.15" = rec {
+        crateName = "syn";
+        version = "2.0.15";
+        edition = "2021";
+        sha256 = "08n8c235bj7f86a5jg561s5zjfijdn8jw6ih2im7xxb0iczcykx3";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "unicode-ident";
+            packageId = "unicode-ident";
+          }
+        ];
+        features = {
+          "default" = [ "derive" "parsing" "printing" "clone-impls" "proc-macro" ];
+          "printing" = [ "quote" ];
+          "proc-macro" = [ "proc-macro2/proc-macro" "quote/proc-macro" ];
+          "quote" = [ "dep:quote" ];
+          "test" = [ "syn-test-suite/all-features" ];
+        };
+        resolvedDefaultFeatures = [ "clone-impls" "default" "derive" "extra-traits" "full" "parsing" "printing" "proc-macro" "quote" "visit-mut" ];
+      };
+      "sync_wrapper" = rec {
+        crateName = "sync_wrapper";
+        version = "0.1.2";
+        edition = "2018";
+        sha256 = "0q01lyj0gr9a93n10nxsn8lwbzq97jqd6b768x17c8f7v7gccir0";
+        authors = [
+          "Actyx AG <developer@actyx.io>"
+        ];
+        features = {
+          "futures" = [ "futures-core" ];
+          "futures-core" = [ "dep:futures-core" ];
+        };
+      };
+      "tabwriter" = rec {
+        crateName = "tabwriter";
+        version = "1.2.1";
+        edition = "2018";
+        sha256 = "048i0mj3b07zlry9m5fl706y5bzdzgrswymqn32drakzk7y5q81n";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "unicode-width";
+            packageId = "unicode-width";
+          }
+        ];
+        features = {
+          "ansi_formatting" = [ "regex" "lazy_static" ];
+          "lazy_static" = [ "dep:lazy_static" ];
+          "regex" = [ "dep:regex" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "tempdir" = rec {
+        crateName = "tempdir";
+        version = "0.3.7";
+        edition = "2015";
+        sha256 = "1n5n86zxpgd85y0mswrp5cfdisizq2rv3la906g6ipyc03xvbwhm";
+        authors = [
+          "The Rust Project Developers"
+        ];
+        dependencies = [
+          {
+            name = "rand";
+            packageId = "rand 0.4.6";
+          }
+          {
+            name = "remove_dir_all";
+            packageId = "remove_dir_all";
+          }
+        ];
+
+      };
+      "tempfile" = rec {
+        crateName = "tempfile";
+        version = "3.5.0";
+        edition = "2018";
+        sha256 = "163rp254r3x7i5hisagrpxid2166pq94jvk511dpkmc1yf2fryxr";
+        authors = [
+          "Steven Allen <steven@stebalien.com>"
+          "The Rust Project Developers"
+          "Ashley Mannix <ashleymannix@live.com.au>"
+          "Jason White <me@jasonwhite.io>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "fastrand";
+            packageId = "fastrand";
+          }
+          {
+            name = "redox_syscall";
+            packageId = "redox_syscall 0.3.5";
+            target = { target, features }: ("redox" == target."os");
+          }
+          {
+            name = "rustix";
+            packageId = "rustix";
+            target = { target, features }: ((target."unix" or false) || ("wasi" == target."os"));
+            features = [ "fs" ];
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.45.0";
+            target = { target, features }: (target."windows" or false);
+            features = [ "Win32_Storage_FileSystem" "Win32_Foundation" ];
+          }
+        ];
+        features = { };
+      };
+      "termcolor" = rec {
+        crateName = "termcolor";
+        version = "1.2.0";
+        edition = "2018";
+        sha256 = "1dmrbsljxpfng905qkaxljlwjhv8h0i3969cbiv5rb7y8a4wymdy";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "winapi-util";
+            packageId = "winapi-util";
+            target = { target, features }: (target."windows" or false);
+          }
+        ];
+
+      };
+      "test-case" = rec {
+        crateName = "test-case";
+        version = "2.2.2";
+        edition = "2018";
+        sha256 = "1h4qymhy332lzgg79w696qfxg6wdab5birn8xvfgkczzgmdczmi1";
+        authors = [
+          "Marcin Sas-Szymanski <marcin.sas-szymanski@anixe.pl>"
+          "Wojciech Polak <frondeus@gmail.com>"
+          "Łukasz Biel <lukasz.p.biel@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "test-case-macros";
+            packageId = "test-case-macros";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "regex" = [ "dep:regex" ];
+          "with-regex" = [ "regex" "test-case-macros/with-regex" ];
+        };
+      };
+      "test-case-macros" = rec {
+        crateName = "test-case-macros";
+        version = "2.2.2";
+        edition = "2018";
+        sha256 = "09jvbfvz48v6ya3i25gp3lbr6ym1fz7qyp3l6bcdslwkw7v7nnz4";
+        procMacro = true;
+        authors = [
+          "Marcin Sas-Szymanski <marcin.sas-szymanski@anixe.pl>"
+          "Wojciech Polak <frondeus@gmail.com>"
+          "Łukasz Biel <lukasz.p.biel@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "proc-macro-error";
+            packageId = "proc-macro-error";
+          }
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "full" "extra-traits" ];
+          }
+        ];
+        features = { };
+      };
+      "test-generator" = rec {
+        crateName = "test-generator";
+        version = "0.3.0";
+        edition = "2018";
+        workspace_member = null;
+        src = pkgs.fetchgit {
+          url = "https://github.com/JamesGuthrie/test-generator.git";
+          rev = "82e799979980962aec1aa324ec6e0e4cad781f41";
+          sha256 = "08brp3qqa55hijc7xby3lam2cc84hvx1zzfqv6lj7smlczh8k32y";
+        };
+        procMacro = true;
+        authors = [
+          "Frank Rehberger <frehberg@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "glob";
+            packageId = "glob";
+          }
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 0.4.30";
+          }
+          {
+            name = "quote";
+            packageId = "quote 0.6.13";
+          }
+          {
+            name = "syn";
+            packageId = "syn 0.15.44";
+            features = [ "full" ];
+          }
+        ];
+
+      };
+      "test-strategy" = rec {
+        crateName = "test-strategy";
+        version = "0.2.1";
+        edition = "2021";
+        sha256 = "105lxqs0vnqff5821sgns8q1scvrwfx1yw6iz7i7nr862j6l1mk2";
+        procMacro = true;
+        authors = [
+          "frozenlib"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "structmeta";
+            packageId = "structmeta";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "visit" "full" ];
+          }
+        ];
+
+      };
+      "text-size" = rec {
+        crateName = "text-size";
+        version = "1.1.0";
+        edition = "2018";
+        sha256 = "02jn26l5wcdjqpy80ycnk9ha10flyc0p4yga8ci6aaz7vd4bb318";
+        authors = [
+          "Aleksey Kladov <aleksey.kladov@gmail.com>"
+          "Christopher Durham (CAD97) <cad97@cad97.com>"
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+        };
+      };
+      "textwrap" = rec {
+        crateName = "textwrap";
+        version = "0.16.0";
+        edition = "2021";
+        sha256 = "0gbwkjf15l6p3x2rkr75fa4cpcs1ly4c8pmlfx5bl6zybcm24ai2";
+        authors = [
+          "Martin Geisler <martin@geisler.net>"
+        ];
+        features = {
+          "default" = [ "unicode-linebreak" "unicode-width" "smawk" ];
+          "hyphenation" = [ "dep:hyphenation" ];
+          "smawk" = [ "dep:smawk" ];
+          "terminal_size" = [ "dep:terminal_size" ];
+          "unicode-linebreak" = [ "dep:unicode-linebreak" ];
+          "unicode-width" = [ "dep:unicode-width" ];
+        };
+      };
+      "thiserror" = rec {
+        crateName = "thiserror";
+        version = "1.0.40";
+        edition = "2018";
+        sha256 = "1b7bdhriasdsr99y39d50jz995xaz9sw3hsbb6z9kp6q9cqrm34p";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "thiserror-impl";
+            packageId = "thiserror-impl";
+          }
+        ];
+
+      };
+      "thiserror-impl" = rec {
+        crateName = "thiserror-impl";
+        version = "1.0.40";
+        edition = "2018";
+        sha256 = "17sn41kyimc6s983aypkk6a45pcyrkbkvrw6rp407n5hqm16ligr";
+        procMacro = true;
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+          }
+        ];
+
+      };
+      "thread_local" = rec {
+        crateName = "thread_local";
+        version = "1.1.7";
+        edition = "2021";
+        sha256 = "0lp19jdgvp5m4l60cgxdnl00yw1hlqy8gcywg9bddwng9h36zp9z";
+        authors = [
+          "Amanieu d'Antras <amanieu@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "once_cell";
+            packageId = "once_cell";
+          }
+        ];
+        features = { };
+      };
+      "tinytemplate" = rec {
+        crateName = "tinytemplate";
+        version = "1.2.1";
+        edition = "2015";
+        sha256 = "1g5n77cqkdh9hy75zdb01adxn45mkh9y40wdr7l68xpz35gnnkdy";
+        authors = [
+          "Brook Heisler <brookheisler@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+          }
+          {
+            name = "serde_json";
+            packageId = "serde_json";
+          }
+        ];
+
+      };
+      "tokio" = rec {
+        crateName = "tokio";
+        version = "1.28.0";
+        edition = "2021";
+        sha256 = "0vqk7dkmvadzqrxwlgja04wlf4s8iymjk6yvcshs7r9lh6zqdiy3";
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+            optional = true;
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+            optional = true;
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "mio";
+            packageId = "mio";
+            optional = true;
+          }
+          {
+            name = "num_cpus";
+            packageId = "num_cpus";
+            optional = true;
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "socket2";
+            packageId = "socket2";
+            optional = true;
+            target = { target, features }: (!(("wasm32" == target."arch") || ("wasm64" == target."arch")));
+            features = [ "all" ];
+          }
+          {
+            name = "tokio-macros";
+            packageId = "tokio-macros";
+            optional = true;
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            target = { target, features }: (target."docsrs" or false);
+            features = [ "Win32_Foundation" "Win32_Security_Authorization" ];
+          }
+          {
+            name = "windows-sys";
+            packageId = "windows-sys 0.48.0";
+            optional = true;
+            target = { target, features }: (target."windows" or false);
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "autocfg";
+            packageId = "autocfg";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+          {
+            name = "socket2";
+            packageId = "socket2";
+            target = { target, features }: (!(("wasm32" == target."arch") || ("wasm64" == target."arch")));
+          }
+        ];
+        features = {
+          "bytes" = [ "dep:bytes" ];
+          "full" = [ "fs" "io-util" "io-std" "macros" "net" "parking_lot" "process" "rt" "rt-multi-thread" "signal" "sync" "time" ];
+          "io-util" = [ "bytes" ];
+          "libc" = [ "dep:libc" ];
+          "macros" = [ "tokio-macros" ];
+          "mio" = [ "dep:mio" ];
+          "net" = [ "libc" "mio/os-poll" "mio/os-ext" "mio/net" "socket2" "windows-sys/Win32_Foundation" "windows-sys/Win32_Security" "windows-sys/Win32_Storage_FileSystem" "windows-sys/Win32_System_Pipes" "windows-sys/Win32_System_SystemServices" ];
+          "num_cpus" = [ "dep:num_cpus" ];
+          "parking_lot" = [ "dep:parking_lot" ];
+          "process" = [ "bytes" "libc" "mio/os-poll" "mio/os-ext" "mio/net" "signal-hook-registry" "windows-sys/Win32_Foundation" "windows-sys/Win32_System_Threading" "windows-sys/Win32_System_WindowsProgramming" ];
+          "rt-multi-thread" = [ "num_cpus" "rt" ];
+          "signal" = [ "libc" "mio/os-poll" "mio/net" "mio/os-ext" "signal-hook-registry" "windows-sys/Win32_Foundation" "windows-sys/Win32_System_Console" ];
+          "signal-hook-registry" = [ "dep:signal-hook-registry" ];
+          "socket2" = [ "dep:socket2" ];
+          "test-util" = [ "rt" "sync" "time" ];
+          "tokio-macros" = [ "dep:tokio-macros" ];
+          "tracing" = [ "dep:tracing" ];
+          "windows-sys" = [ "dep:windows-sys" ];
+        };
+        resolvedDefaultFeatures = [ "bytes" "default" "io-std" "io-util" "libc" "macros" "mio" "net" "num_cpus" "rt" "rt-multi-thread" "socket2" "sync" "time" "tokio-macros" "windows-sys" ];
+      };
+      "tokio-io-timeout" = rec {
+        crateName = "tokio-io-timeout";
+        version = "1.2.0";
+        edition = "2018";
+        sha256 = "1gx84f92q1491vj4pkn81j8pz1s3pgwnbrsdhfsa2556mli41drh";
+        authors = [
+          "Steven Fackler <sfackler@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "time" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "full" ];
+          }
+        ];
+
+      };
+      "tokio-macros" = rec {
+        crateName = "tokio-macros";
+        version = "2.1.0";
+        edition = "2018";
+        sha256 = "0pk7y9dfanab886iaqwcbri39jkw33kgl7y07v0kg1pp8prdq2v3";
+        procMacro = true;
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+            features = [ "full" ];
+          }
+        ];
+
+      };
+      "tokio-stream" = rec {
+        crateName = "tokio-stream";
+        version = "0.1.14";
+        edition = "2021";
+        sha256 = "0hi8hcwavh5sdi1ivc9qc4yvyr32f153c212dpd7sb366y6rhz1r";
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "sync" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "full" "test-util" ];
+          }
+        ];
+        features = {
+          "default" = [ "time" ];
+          "fs" = [ "tokio/fs" ];
+          "full" = [ "time" "net" "io-util" "fs" "sync" "signal" ];
+          "io-util" = [ "tokio/io-util" ];
+          "net" = [ "tokio/net" ];
+          "signal" = [ "tokio/signal" ];
+          "sync" = [ "tokio/sync" "tokio-util" ];
+          "time" = [ "tokio/time" ];
+          "tokio-util" = [ "dep:tokio-util" ];
+        };
+        resolvedDefaultFeatures = [ "default" "net" "time" ];
+      };
+      "tokio-util" = rec {
+        crateName = "tokio-util";
+        version = "0.7.8";
+        edition = "2021";
+        sha256 = "0zfvblvskaj65rianw6agz9hnbdbsfgf5fkw4srwik3yr31fhvw0";
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+          }
+          {
+            name = "futures-sink";
+            packageId = "futures-sink";
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "sync" ];
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "full" ];
+          }
+        ];
+        features = {
+          "__docs_rs" = [ "futures-util" ];
+          "codec" = [ "tracing" ];
+          "compat" = [ "futures-io" ];
+          "full" = [ "codec" "compat" "io-util" "time" "net" "rt" ];
+          "futures-io" = [ "dep:futures-io" ];
+          "futures-util" = [ "dep:futures-util" ];
+          "hashbrown" = [ "dep:hashbrown" ];
+          "io-util" = [ "io" "tokio/rt" "tokio/io-util" ];
+          "net" = [ "tokio/net" ];
+          "rt" = [ "tokio/rt" "tokio/sync" "futures-util" "hashbrown" ];
+          "slab" = [ "dep:slab" ];
+          "time" = [ "tokio/time" "slab" ];
+          "tracing" = [ "dep:tracing" ];
+        };
+        resolvedDefaultFeatures = [ "codec" "default" "io" "io-util" "tracing" ];
+      };
+      "toml" = rec {
+        crateName = "toml";
+        version = "0.6.0";
+        edition = "2021";
+        sha256 = "05zjz69wjymp9yrgccg5vhvxpf855rgn23vl1yvri4nwwj8difag";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+          }
+          {
+            name = "serde_spanned";
+            packageId = "serde_spanned";
+            features = [ "serde" ];
+          }
+          {
+            name = "toml_datetime";
+            packageId = "toml_datetime";
+            features = [ "serde" ];
+          }
+          {
+            name = "toml_edit";
+            packageId = "toml_edit";
+            optional = true;
+            features = [ "serde" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "derive" ];
+          }
+        ];
+        features = {
+          "default" = [ "parse" "display" ];
+          "display" = [ "dep:toml_edit" ];
+          "indexmap" = [ "dep:indexmap" ];
+          "parse" = [ "dep:toml_edit" ];
+          "preserve_order" = [ "indexmap" ];
+        };
+        resolvedDefaultFeatures = [ "default" "display" "parse" ];
+      };
+      "toml_datetime" = rec {
+        crateName = "toml_datetime";
+        version = "0.5.1";
+        edition = "2021";
+        sha256 = "1xcw3kyklh3s2gxp65ma26rgkl7505la4xx1r55kfgcfmikz8ls5";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+            optional = true;
+          }
+        ];
+        features = {
+          "serde" = [ "dep:serde" ];
+        };
+        resolvedDefaultFeatures = [ "serde" ];
+      };
+      "toml_edit" = rec {
+        crateName = "toml_edit";
+        version = "0.18.1";
+        edition = "2021";
+        sha256 = "0ax1bwzd4xclpids3b69nd1nxqi3x3qa4ymz51jbrp6hsy6rvian";
+        authors = [
+          "Andronik Ordian <write@reusable.software>"
+          "Ed Page <eopage@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "indexmap";
+            packageId = "indexmap";
+          }
+          {
+            name = "nom8";
+            packageId = "nom8";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            optional = true;
+          }
+          {
+            name = "serde_spanned";
+            packageId = "serde_spanned";
+            optional = true;
+            features = [ "serde" ];
+          }
+          {
+            name = "toml_datetime";
+            packageId = "toml_datetime";
+          }
+        ];
+        features = {
+          "easy" = [ "serde" ];
+          "perf" = [ "dep:kstring" ];
+          "serde" = [ "dep:serde" "toml_datetime/serde" "dep:serde_spanned" ];
+        };
+        resolvedDefaultFeatures = [ "default" "serde" ];
+      };
+      "tonic" = rec {
+        crateName = "tonic";
+        version = "0.8.3";
+        edition = "2018";
+        sha256 = "1yymp2xi1p60g81p5jfaybcawpfkb01vqvzqn4cyz6wj7fnry8cg";
+        authors = [
+          "Lucio Franco <luciofranco14@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "async-stream";
+            packageId = "async-stream";
+          }
+          {
+            name = "async-trait";
+            packageId = "async-trait";
+            optional = true;
+          }
+          {
+            name = "axum";
+            packageId = "axum";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "base64";
+            packageId = "base64 0.13.1";
+          }
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "h2";
+            packageId = "h2";
+            optional = true;
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "http-body";
+            packageId = "http-body";
+          }
+          {
+            name = "hyper";
+            packageId = "hyper";
+            optional = true;
+            features = [ "full" ];
+          }
+          {
+            name = "hyper-timeout";
+            packageId = "hyper-timeout";
+            optional = true;
+          }
+          {
+            name = "percent-encoding";
+            packageId = "percent-encoding";
+          }
+          {
+            name = "pin-project";
+            packageId = "pin-project";
+          }
+          {
+            name = "prost";
+            packageId = "prost";
+            rename = "prost1";
+            optional = true;
+          }
+          {
+            name = "prost-derive";
+            packageId = "prost-derive";
+            optional = true;
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            optional = true;
+            features = [ "net" ];
+          }
+          {
+            name = "tokio-stream";
+            packageId = "tokio-stream";
+          }
+          {
+            name = "tokio-util";
+            packageId = "tokio-util";
+            features = [ "codec" ];
+          }
+          {
+            name = "tower";
+            packageId = "tower";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "balance" "buffer" "discover" "limit" "load" "make" "timeout" "util" ];
+          }
+          {
+            name = "tower-layer";
+            packageId = "tower-layer";
+          }
+          {
+            name = "tower-service";
+            packageId = "tower-service";
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+          }
+          {
+            name = "tracing-futures";
+            packageId = "tracing-futures";
+            optional = true;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "rt" "macros" ];
+          }
+          {
+            name = "tower";
+            packageId = "tower";
+            features = [ "full" ];
+          }
+        ];
+        features = {
+          "async-trait" = [ "dep:async-trait" ];
+          "axum" = [ "dep:axum" ];
+          "channel" = [ "h2" "hyper" "tokio" "tower" "tracing-futures" "tokio/macros" "tokio/time" "hyper-timeout" ];
+          "codegen" = [ "async-trait" ];
+          "default" = [ "transport" "codegen" "prost" ];
+          "flate2" = [ "dep:flate2" ];
+          "gzip" = [ "flate2" ];
+          "h2" = [ "dep:h2" ];
+          "hyper" = [ "dep:hyper" ];
+          "hyper-timeout" = [ "dep:hyper-timeout" ];
+          "prost" = [ "prost1" "prost-derive" ];
+          "prost-derive" = [ "dep:prost-derive" ];
+          "prost1" = [ "dep:prost1" ];
+          "rustls-native-certs" = [ "dep:rustls-native-certs" ];
+          "rustls-pemfile" = [ "dep:rustls-pemfile" ];
+          "tls" = [ "rustls-pemfile" "transport" "tokio-rustls" ];
+          "tls-roots" = [ "tls-roots-common" "rustls-native-certs" ];
+          "tls-roots-common" = [ "tls" ];
+          "tls-webpki-roots" = [ "tls-roots-common" "webpki-roots" ];
+          "tokio" = [ "dep:tokio" ];
+          "tokio-rustls" = [ "dep:tokio-rustls" ];
+          "tower" = [ "dep:tower" ];
+          "tracing-futures" = [ "dep:tracing-futures" ];
+          "transport" = [ "axum" "channel" ];
+          "webpki-roots" = [ "dep:webpki-roots" ];
+        };
+        resolvedDefaultFeatures = [ "async-trait" "axum" "channel" "codegen" "default" "h2" "hyper" "hyper-timeout" "prost" "prost-derive" "prost1" "tokio" "tower" "tracing-futures" "transport" ];
+      };
+      "tonic-build" = rec {
+        crateName = "tonic-build";
+        version = "0.8.4";
+        edition = "2018";
+        sha256 = "1i781mfzcbzfk6fnf7qp95q6r0b05ixvmynw4z0agq7pq2wykxav";
+        authors = [
+          "Lucio Franco <luciofranco14@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "prettyplease";
+            packageId = "prettyplease";
+          }
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "prost-build";
+            packageId = "prost-build";
+            optional = true;
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+          }
+        ];
+        features = {
+          "cleanup-markdown" = [ "prost-build/cleanup-markdown" ];
+          "default" = [ "transport" "prost" ];
+          "prost" = [ "prost-build" ];
+          "prost-build" = [ "dep:prost-build" ];
+        };
+        resolvedDefaultFeatures = [ "default" "prost" "prost-build" "transport" ];
+      };
+      "tonic-mock" = rec {
+        crateName = "tonic-mock";
+        version = "0.1.0";
+        edition = "2018";
+        workspace_member = null;
+        src = pkgs.fetchgit {
+          url = "https://github.com/brainrake/tonic-mock";
+          rev = "ec1a15510875de99d709d684190db5d9beab175e";
+          sha256 = "0lwa03hpp0mxa6aa1zv5w68k61y4hccfm0q2ykyq392fwal8vb50";
+        };
+        authors = [
+          "Tyr Chen <tyr.chen@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "futures";
+            packageId = "futures";
+          }
+          {
+            name = "http";
+            packageId = "http";
+          }
+          {
+            name = "http-body";
+            packageId = "http-body";
+          }
+          {
+            name = "prost";
+            packageId = "prost";
+          }
+          {
+            name = "tonic";
+            packageId = "tonic";
+          }
+        ];
+
+      };
+      "tonic-reflection" = rec {
+        crateName = "tonic-reflection";
+        version = "0.5.0";
+        edition = "2018";
+        sha256 = "05i6c1fcbwkkj0p2r63vb5iac60b22a5rif3zx5li8a0slqgfm84";
+        authors = [
+          "James Nugent <james@jen20.com>"
+          "Samani G. Gikandi <samani@gojulas.com>"
+        ];
+        dependencies = [
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "prost";
+            packageId = "prost";
+          }
+          {
+            name = "prost-types";
+            packageId = "prost-types";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "sync" ];
+          }
+          {
+            name = "tokio-stream";
+            packageId = "tokio-stream";
+            features = [ "net" ];
+          }
+          {
+            name = "tonic";
+            packageId = "tonic";
+            features = [ "codegen" "prost" ];
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "tonic-build";
+            packageId = "tonic-build";
+            features = [ "transport" "prost" ];
+          }
+        ];
+
+      };
+      "tower" = rec {
+        crateName = "tower";
+        version = "0.4.13";
+        edition = "2018";
+        sha256 = "073wncyqav4sak1p755hf6vl66njgfc1z1g1di9rxx3cvvh9pymq";
+        authors = [
+          "Tower Maintainers <team@tower-rs.com>"
+        ];
+        dependencies = [
+          {
+            name = "futures-core";
+            packageId = "futures-core";
+            optional = true;
+          }
+          {
+            name = "futures-util";
+            packageId = "futures-util";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "alloc" ];
+          }
+          {
+            name = "indexmap";
+            packageId = "indexmap";
+            optional = true;
+          }
+          {
+            name = "pin-project";
+            packageId = "pin-project";
+            optional = true;
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+            optional = true;
+          }
+          {
+            name = "rand";
+            packageId = "rand 0.8.5";
+            optional = true;
+            features = [ "small_rng" ];
+          }
+          {
+            name = "slab";
+            packageId = "slab";
+            optional = true;
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            optional = true;
+            features = [ "sync" ];
+          }
+          {
+            name = "tokio-util";
+            packageId = "tokio-util";
+            optional = true;
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "tower-layer";
+            packageId = "tower-layer";
+          }
+          {
+            name = "tower-service";
+            packageId = "tower-service";
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "macros" "sync" "test-util" "rt-multi-thread" ];
+          }
+        ];
+        features = {
+          "__common" = [ "futures-core" "pin-project-lite" ];
+          "balance" = [ "discover" "load" "ready-cache" "make" "rand" "slab" ];
+          "buffer" = [ "__common" "tokio/sync" "tokio/rt" "tokio-util" "tracing" ];
+          "default" = [ "log" ];
+          "discover" = [ "__common" ];
+          "filter" = [ "__common" "futures-util" ];
+          "full" = [ "balance" "buffer" "discover" "filter" "hedge" "limit" "load" "load-shed" "make" "ready-cache" "reconnect" "retry" "spawn-ready" "steer" "timeout" "util" ];
+          "futures-core" = [ "dep:futures-core" ];
+          "futures-util" = [ "dep:futures-util" ];
+          "hdrhistogram" = [ "dep:hdrhistogram" ];
+          "hedge" = [ "util" "filter" "futures-util" "hdrhistogram" "tokio/time" "tracing" ];
+          "indexmap" = [ "dep:indexmap" ];
+          "limit" = [ "__common" "tokio/time" "tokio/sync" "tokio-util" "tracing" ];
+          "load" = [ "__common" "tokio/time" "tracing" ];
+          "load-shed" = [ "__common" ];
+          "log" = [ "tracing/log" ];
+          "make" = [ "futures-util" "pin-project-lite" "tokio/io-std" ];
+          "pin-project" = [ "dep:pin-project" ];
+          "pin-project-lite" = [ "dep:pin-project-lite" ];
+          "rand" = [ "dep:rand" ];
+          "ready-cache" = [ "futures-core" "futures-util" "indexmap" "tokio/sync" "tracing" "pin-project-lite" ];
+          "reconnect" = [ "make" "tokio/io-std" "tracing" ];
+          "retry" = [ "__common" "tokio/time" ];
+          "slab" = [ "dep:slab" ];
+          "spawn-ready" = [ "__common" "futures-util" "tokio/sync" "tokio/rt" "util" "tracing" ];
+          "timeout" = [ "pin-project-lite" "tokio/time" ];
+          "tokio" = [ "dep:tokio" ];
+          "tokio-stream" = [ "dep:tokio-stream" ];
+          "tokio-util" = [ "dep:tokio-util" ];
+          "tracing" = [ "dep:tracing" ];
+          "util" = [ "__common" "futures-util" "pin-project" ];
+        };
+        resolvedDefaultFeatures = [ "__common" "balance" "buffer" "default" "discover" "futures-core" "futures-util" "indexmap" "limit" "load" "log" "make" "pin-project" "pin-project-lite" "rand" "ready-cache" "slab" "timeout" "tokio" "tokio-util" "tracing" "util" ];
+      };
+      "tower-layer" = rec {
+        crateName = "tower-layer";
+        version = "0.3.2";
+        edition = "2018";
+        sha256 = "1l7i17k9vlssrdg4s3b0ia5jjkmmxsvv8s9y9ih0jfi8ssz8s362";
+        authors = [
+          "Tower Maintainers <team@tower-rs.com>"
+        ];
+
+      };
+      "tower-service" = rec {
+        crateName = "tower-service";
+        version = "0.3.2";
+        edition = "2018";
+        sha256 = "0lmfzmmvid2yp2l36mbavhmqgsvzqf7r2wiwz73ml4xmwaf1rg5n";
+        authors = [
+          "Tower Maintainers <team@tower-rs.com>"
+        ];
+
+      };
+      "tracing" = rec {
+        crateName = "tracing";
+        version = "0.1.37";
+        edition = "2018";
+        sha256 = "1f2fylc79xmbh7v53kak6qyw27njbx227rd64kb4bga8ilxc7s4c";
+        authors = [
+          "Eliza Weisman <eliza@buoyant.io>"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "log";
+            packageId = "log";
+            optional = true;
+          }
+          {
+            name = "pin-project-lite";
+            packageId = "pin-project-lite";
+          }
+          {
+            name = "tracing-attributes";
+            packageId = "tracing-attributes";
+            optional = true;
+          }
+          {
+            name = "tracing-core";
+            packageId = "tracing-core";
+            usesDefaultFeatures = false;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "log";
+            packageId = "log";
+          }
+        ];
+        features = {
+          "attributes" = [ "tracing-attributes" ];
+          "default" = [ "std" "attributes" ];
+          "log" = [ "dep:log" ];
+          "log-always" = [ "log" ];
+          "std" = [ "tracing-core/std" ];
+          "tracing-attributes" = [ "dep:tracing-attributes" ];
+          "valuable" = [ "tracing-core/valuable" ];
+        };
+        resolvedDefaultFeatures = [ "attributes" "default" "log" "std" "tracing-attributes" ];
+      };
+      "tracing-attributes" = rec {
+        crateName = "tracing-attributes";
+        version = "0.1.24";
+        edition = "2018";
+        sha256 = "0x3spb5h4m56035lrvrchbyhg8pxrg4sk0qij8d0ni815b5f6mqg";
+        procMacro = true;
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+          "Eliza Weisman <eliza@buoyant.io>"
+          "David Barsky <dbarsky@amazon.com>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 2.0.15";
+            usesDefaultFeatures = false;
+            features = [ "full" "parsing" "printing" "visit-mut" "clone-impls" "extra-traits" "proc-macro" ];
+          }
+        ];
+        features = { };
+      };
+      "tracing-core" = rec {
+        crateName = "tracing-core";
+        version = "0.1.30";
+        edition = "2018";
+        sha256 = "0fi1jz3jbzk3n7k379pwv3wfhn35c5gcwn000m2xh7xb1sx07sr4";
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "once_cell";
+            packageId = "once_cell";
+            optional = true;
+          }
+          {
+            name = "valuable";
+            packageId = "valuable";
+            optional = true;
+            usesDefaultFeatures = false;
+            target = { target, features }: (target."tracing_unstable" or false);
+          }
+        ];
+        features = {
+          "default" = [ "std" "valuable/std" ];
+          "once_cell" = [ "dep:once_cell" ];
+          "std" = [ "once_cell" ];
+          "valuable" = [ "dep:valuable" ];
+        };
+        resolvedDefaultFeatures = [ "default" "once_cell" "std" "valuable" ];
+      };
+      "tracing-futures" = rec {
+        crateName = "tracing-futures";
+        version = "0.2.5";
+        edition = "2018";
+        sha256 = "1wimg0iwa2ldq7xv98lvivvf3q9ykfminig8r1bs0ig22np9bl4p";
+        authors = [
+          "Eliza Weisman <eliza@buoyant.io>"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "pin-project";
+            packageId = "pin-project";
+            optional = true;
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "default" = [ "std-future" "std" ];
+          "futures" = [ "dep:futures" ];
+          "futures-01" = [ "futures_01" "std" ];
+          "futures-03" = [ "std-future" "futures" "futures-task" "std" ];
+          "futures-task" = [ "dep:futures-task" ];
+          "futures_01" = [ "dep:futures_01" ];
+          "pin-project" = [ "dep:pin-project" ];
+          "std" = [ "tracing/std" ];
+          "std-future" = [ "pin-project" ];
+          "tokio" = [ "dep:tokio" ];
+          "tokio-executor" = [ "dep:tokio-executor" ];
+        };
+        resolvedDefaultFeatures = [ "default" "pin-project" "std" "std-future" ];
+      };
+      "tracing-log" = rec {
+        crateName = "tracing-log";
+        version = "0.1.3";
+        edition = "2018";
+        sha256 = "08prnkxq8yas6jvvjnvyx5v3hwblas5527wxxgbiw2yis8rsvpbq";
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+          }
+          {
+            name = "log";
+            packageId = "log";
+          }
+          {
+            name = "tracing-core";
+            packageId = "tracing-core";
+          }
+        ];
+        features = {
+          "ahash" = [ "dep:ahash" ];
+          "default" = [ "log-tracer" "trace-logger" "std" ];
+          "env_logger" = [ "dep:env_logger" ];
+          "interest-cache" = [ "lru" "ahash" ];
+          "lru" = [ "dep:lru" ];
+          "std" = [ "log/std" ];
+        };
+        resolvedDefaultFeatures = [ "log-tracer" "std" ];
+      };
+      "tracing-serde" = rec {
+        crateName = "tracing-serde";
+        version = "0.1.3";
+        edition = "2018";
+        sha256 = "1qfr0va69djvxqvjrx4vqq7p6myy414lx4w1f6amcn0hfwqj2sxw";
+        authors = [
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+          }
+          {
+            name = "tracing-core";
+            packageId = "tracing-core";
+          }
+        ];
+        features = {
+          "valuable" = [ "valuable_crate" "valuable-serde" "tracing-core/valuable" ];
+          "valuable-serde" = [ "dep:valuable-serde" ];
+          "valuable_crate" = [ "dep:valuable_crate" ];
+        };
+      };
+      "tracing-subscriber" = rec {
+        crateName = "tracing-subscriber";
+        version = "0.3.17";
+        edition = "2018";
+        sha256 = "0xvwfpmb943hdy4gzyn7a2azgigf30mfd1kx10gyh5gr6yy539ih";
+        authors = [
+          "Eliza Weisman <eliza@buoyant.io>"
+          "David Barsky <me@davidbarsky.com>"
+          "Tokio Contributors <team@tokio.rs>"
+        ];
+        dependencies = [
+          {
+            name = "nu-ansi-term";
+            packageId = "nu-ansi-term";
+            optional = true;
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            optional = true;
+          }
+          {
+            name = "serde_json";
+            packageId = "serde_json";
+            optional = true;
+          }
+          {
+            name = "sharded-slab";
+            packageId = "sharded-slab";
+            optional = true;
+          }
+          {
+            name = "smallvec";
+            packageId = "smallvec";
+            optional = true;
+          }
+          {
+            name = "thread_local";
+            packageId = "thread_local";
+            optional = true;
+          }
+          {
+            name = "tracing-core";
+            packageId = "tracing-core";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "tracing-log";
+            packageId = "tracing-log";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "log-tracer" "std" ];
+          }
+          {
+            name = "tracing-serde";
+            packageId = "tracing-serde";
+            optional = true;
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tracing-log";
+            packageId = "tracing-log";
+          }
+        ];
+        features = {
+          "ansi" = [ "fmt" "nu-ansi-term" ];
+          "default" = [ "smallvec" "fmt" "ansi" "tracing-log" "std" ];
+          "env-filter" = [ "matchers" "regex" "once_cell" "tracing" "std" "thread_local" ];
+          "fmt" = [ "registry" "std" ];
+          "json" = [ "tracing-serde" "serde" "serde_json" ];
+          "local-time" = [ "time/local-offset" ];
+          "matchers" = [ "dep:matchers" ];
+          "nu-ansi-term" = [ "dep:nu-ansi-term" ];
+          "once_cell" = [ "dep:once_cell" ];
+          "parking_lot" = [ "dep:parking_lot" ];
+          "regex" = [ "dep:regex" ];
+          "registry" = [ "sharded-slab" "thread_local" "std" ];
+          "serde" = [ "dep:serde" ];
+          "serde_json" = [ "dep:serde_json" ];
+          "sharded-slab" = [ "dep:sharded-slab" ];
+          "smallvec" = [ "dep:smallvec" ];
+          "std" = [ "alloc" "tracing-core/std" ];
+          "thread_local" = [ "dep:thread_local" ];
+          "time" = [ "dep:time" ];
+          "tracing" = [ "dep:tracing" ];
+          "tracing-log" = [ "dep:tracing-log" ];
+          "tracing-serde" = [ "dep:tracing-serde" ];
+          "valuable" = [ "tracing-core/valuable" "valuable_crate" "valuable-serde" "tracing-serde/valuable" ];
+          "valuable-serde" = [ "dep:valuable-serde" ];
+          "valuable_crate" = [ "dep:valuable_crate" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "ansi" "default" "fmt" "json" "nu-ansi-term" "registry" "serde" "serde_json" "sharded-slab" "smallvec" "std" "thread_local" "tracing-log" "tracing-serde" ];
+      };
+      "try-lock" = rec {
+        crateName = "try-lock";
+        version = "0.2.4";
+        edition = "2015";
+        sha256 = "1vc15paa4zi06ixsxihwbvfn24d708nsyg1ncgqwcrn42byyqa1m";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+
+      };
+      "tvix-cli" = rec {
+        crateName = "tvix-cli";
+        version = "0.1.0";
+        edition = "2021";
+        crateBin = [
+          {
+            name = "tvix";
+            path = "src/main.rs";
+            requiredFeatures = [ ];
+          }
+        ];
+        # We can't filter paths with references in Nix 2.4
+        # See https://github.com/NixOS/nix/issues/5410
+        src =
+          if (lib.versionOlder builtins.nixVersion "2.4pre20211007")
+          then lib.cleanSourceWith { filter = sourceFilter; src = ./cli; }
+          else ./cli;
+        dependencies = [
+          {
+            name = "clap";
+            packageId = "clap 4.2.7";
+            features = [ "derive" "env" ];
+          }
+          {
+            name = "data-encoding";
+            packageId = "data-encoding";
+          }
+          {
+            name = "dirs";
+            packageId = "dirs";
+          }
+          {
+            name = "nix-compat";
+            packageId = "nix-compat";
+          }
+          {
+            name = "rustyline";
+            packageId = "rustyline";
+          }
+          {
+            name = "smol_str";
+            packageId = "smol_str";
+          }
+          {
+            name = "ssri";
+            packageId = "ssri";
+          }
+          {
+            name = "thiserror";
+            packageId = "thiserror";
+          }
+          {
+            name = "tvix-eval";
+            packageId = "tvix-eval";
+          }
+          {
+            name = "tvix-store";
+            packageId = "tvix-store";
+          }
+          {
+            name = "wu-manber";
+            packageId = "wu-manber";
+          }
+        ];
+
+      };
+      "tvix-eval" = rec {
+        crateName = "tvix-eval";
+        version = "0.1.0";
+        edition = "2021";
+        # We can't filter paths with references in Nix 2.4
+        # See https://github.com/NixOS/nix/issues/5410
+        src =
+          if (lib.versionOlder builtins.nixVersion "2.4pre20211007")
+          then lib.cleanSourceWith { filter = sourceFilter; src = ./eval; }
+          else ./eval;
+        libName = "tvix_eval";
+        dependencies = [
+          {
+            name = "codemap";
+            packageId = "codemap";
+          }
+          {
+            name = "codemap-diagnostic";
+            packageId = "codemap-diagnostic";
+          }
+          {
+            name = "dirs";
+            packageId = "dirs";
+          }
+          {
+            name = "genawaiter";
+            packageId = "genawaiter";
+            usesDefaultFeatures = false;
+          }
+          {
+            name = "imbl";
+            packageId = "imbl";
+            features = [ "serde" ];
+          }
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+          }
+          {
+            name = "lexical-core";
+            packageId = "lexical-core";
+            features = [ "format" "parse-floats" ];
+          }
+          {
+            name = "path-clean";
+            packageId = "path-clean";
+          }
+          {
+            name = "proptest";
+            packageId = "proptest";
+            optional = true;
+            usesDefaultFeatures = false;
+            features = [ "std" "alloc" "break-dead-code" "tempfile" ];
+          }
+          {
+            name = "regex";
+            packageId = "regex";
+          }
+          {
+            name = "rnix";
+            packageId = "rnix";
+          }
+          {
+            name = "rowan";
+            packageId = "rowan";
+          }
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "rc" "derive" ];
+          }
+          {
+            name = "serde_json";
+            packageId = "serde_json";
+          }
+          {
+            name = "smol_str";
+            packageId = "smol_str";
+          }
+          {
+            name = "tabwriter";
+            packageId = "tabwriter";
+          }
+          {
+            name = "test-strategy";
+            packageId = "test-strategy";
+            optional = true;
+          }
+          {
+            name = "toml";
+            packageId = "toml";
+          }
+          {
+            name = "tvix-eval-builtin-macros";
+            packageId = "tvix-eval-builtin-macros";
+            rename = "builtin-macros";
+          }
+          {
+            name = "xml-rs";
+            packageId = "xml-rs";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "criterion";
+            packageId = "criterion";
+          }
+          {
+            name = "itertools";
+            packageId = "itertools";
+          }
+          {
+            name = "pretty_assertions";
+            packageId = "pretty_assertions";
+          }
+          {
+            name = "tempdir";
+            packageId = "tempdir";
+          }
+          {
+            name = "test-generator";
+            packageId = "test-generator";
+          }
+        ];
+        features = {
+          "arbitrary" = [ "proptest" "test-strategy" "imbl/proptest" ];
+          "default" = [ "impure" "arbitrary" "nix_tests" ];
+          "proptest" = [ "dep:proptest" ];
+          "test-strategy" = [ "dep:test-strategy" ];
+        };
+        resolvedDefaultFeatures = [ "arbitrary" "default" "impure" "nix_tests" "proptest" "test-strategy" ];
+      };
+      "tvix-eval-builtin-macros" = rec {
+        crateName = "tvix-eval-builtin-macros";
+        version = "0.0.1";
+        edition = "2021";
+        # We can't filter paths with references in Nix 2.4
+        # See https://github.com/NixOS/nix/issues/5410
+        src =
+          if (lib.versionOlder builtins.nixVersion "2.4pre20211007")
+          then lib.cleanSourceWith { filter = sourceFilter; src = ./eval/builtin-macros; }
+          else ./eval/builtin-macros;
+        procMacro = true;
+        authors = [
+          "Griffin Smith <root@gws.fyi>"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "full" "parsing" "printing" "visit" "visit-mut" "extra-traits" ];
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tvix-eval";
+            packageId = "tvix-eval";
+          }
+        ];
+
+      };
+      "tvix-serde" = rec {
+        crateName = "tvix-serde";
+        version = "0.1.0";
+        edition = "2021";
+        # We can't filter paths with references in Nix 2.4
+        # See https://github.com/NixOS/nix/issues/5410
+        src =
+          if (lib.versionOlder builtins.nixVersion "2.4pre20211007")
+          then lib.cleanSourceWith { filter = sourceFilter; src = ./serde; }
+          else ./serde;
+        dependencies = [
+          {
+            name = "serde";
+            packageId = "serde";
+            features = [ "derive" ];
+          }
+          {
+            name = "tvix-eval";
+            packageId = "tvix-eval";
+          }
+        ];
+
+      };
+      "tvix-store" = rec {
+        crateName = "tvix-store";
+        version = "0.1.0";
+        edition = "2021";
+        crateBin = [
+          {
+            name = "tvix-store";
+            path = "src/bin/tvix-store.rs";
+            requiredFeatures = [ ];
+          }
+        ];
+        # We can't filter paths with references in Nix 2.4
+        # See https://github.com/NixOS/nix/issues/5410
+        src =
+          if (lib.versionOlder builtins.nixVersion "2.4pre20211007")
+          then lib.cleanSourceWith { filter = sourceFilter; src = ./store; }
+          else ./store;
+        dependencies = [
+          {
+            name = "anyhow";
+            packageId = "anyhow";
+          }
+          {
+            name = "blake3";
+            packageId = "blake3";
+            features = [ "rayon" "std" ];
+          }
+          {
+            name = "bytes";
+            packageId = "bytes";
+          }
+          {
+            name = "clap";
+            packageId = "clap 4.2.7";
+            features = [ "derive" "env" ];
+          }
+          {
+            name = "count-write";
+            packageId = "count-write";
+          }
+          {
+            name = "data-encoding";
+            packageId = "data-encoding";
+          }
+          {
+            name = "futures";
+            packageId = "futures";
+          }
+          {
+            name = "lazy_static";
+            packageId = "lazy_static";
+          }
+          {
+            name = "nix-compat";
+            packageId = "nix-compat";
+          }
+          {
+            name = "prost";
+            packageId = "prost";
+          }
+          {
+            name = "rayon";
+            packageId = "rayon";
+          }
+          {
+            name = "serde_json";
+            packageId = "serde_json";
+          }
+          {
+            name = "sha2";
+            packageId = "sha2 0.10.6";
+          }
+          {
+            name = "sled";
+            packageId = "sled";
+            features = [ "compression" ];
+          }
+          {
+            name = "smol_str";
+            packageId = "smol_str";
+          }
+          {
+            name = "thiserror";
+            packageId = "thiserror";
+          }
+          {
+            name = "tokio";
+            packageId = "tokio";
+            features = [ "rt-multi-thread" "net" ];
+          }
+          {
+            name = "tokio-stream";
+            packageId = "tokio-stream";
+          }
+          {
+            name = "tokio-util";
+            packageId = "tokio-util";
+            features = [ "io" "io-util" ];
+          }
+          {
+            name = "tonic";
+            packageId = "tonic";
+          }
+          {
+            name = "tonic-reflection";
+            packageId = "tonic-reflection";
+            optional = true;
+          }
+          {
+            name = "tower";
+            packageId = "tower";
+          }
+          {
+            name = "tracing";
+            packageId = "tracing";
+          }
+          {
+            name = "tracing-subscriber";
+            packageId = "tracing-subscriber";
+            features = [ "json" ];
+          }
+          {
+            name = "tvix-eval";
+            packageId = "tvix-eval";
+          }
+          {
+            name = "walkdir";
+            packageId = "walkdir";
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "prost-build";
+            packageId = "prost-build";
+          }
+          {
+            name = "tonic-build";
+            packageId = "tonic-build";
+          }
+        ];
+        devDependencies = [
+          {
+            name = "tempfile";
+            packageId = "tempfile";
+          }
+          {
+            name = "test-case";
+            packageId = "test-case";
+          }
+          {
+            name = "tonic-mock";
+            packageId = "tonic-mock";
+          }
+        ];
+        features = {
+          "default" = [ "reflection" ];
+          "reflection" = [ "tonic-reflection" ];
+          "tonic-reflection" = [ "dep:tonic-reflection" ];
+        };
+        resolvedDefaultFeatures = [ "default" "reflection" "tonic-reflection" ];
+      };
+      "typenum" = rec {
+        crateName = "typenum";
+        version = "1.16.0";
+        edition = "2018";
+        sha256 = "1fhb9iaqyjn4dzn2vl86kxjhp4xpw5gynczlnqzf4x6rjgpn2ya9";
+        build = "build/main.rs";
+        authors = [
+          "Paho Lurie-Gregg <paho@paholg.com>"
+          "Andre Bogus <bogusandre@gmail.com>"
+        ];
+        features = {
+          "scale-info" = [ "dep:scale-info" ];
+          "scale_info" = [ "scale-info/derive" ];
+        };
+      };
+      "unarray" = rec {
+        crateName = "unarray";
+        version = "0.1.4";
+        edition = "2018";
+        sha256 = "154smf048k84prsdgh09nkm2n0w0336v84jd4zikyn6v6jrqbspa";
+
+      };
+      "unicode-ident" = rec {
+        crateName = "unicode-ident";
+        version = "1.0.8";
+        edition = "2018";
+        sha256 = "1x4v4v95fv9gn5zbpm23sa9awjvmclap1wh1lmikmw9rna3llip5";
+        authors = [
+          "David Tolnay <dtolnay@gmail.com>"
+        ];
+
+      };
+      "unicode-segmentation" = rec {
+        crateName = "unicode-segmentation";
+        version = "1.10.1";
+        edition = "2018";
+        sha256 = "0dky2hm5k51xy11hc3nk85p533rvghd462b6i0c532b7hl4j9mhx";
+        authors = [
+          "kwantam <kwantam@gmail.com>"
+          "Manish Goregaokar <manishsmail@gmail.com>"
+        ];
+        features = { };
+      };
+      "unicode-width" = rec {
+        crateName = "unicode-width";
+        version = "0.1.10";
+        edition = "2015";
+        sha256 = "12vc3wv0qwg8rzcgb9bhaf5119dlmd6lmkhbfy1zfls6n7jx3vf0";
+        authors = [
+          "kwantam <kwantam@gmail.com>"
+          "Manish Goregaokar <manishsmail@gmail.com>"
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "rustc-dep-of-std" = [ "std" "core" "compiler_builtins" ];
+          "std" = [ "dep:std" ];
+        };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "unicode-xid" = rec {
+        crateName = "unicode-xid";
+        version = "0.1.0";
+        edition = "2015";
+        sha256 = "1z57lqh4s18rr4x0j4fw4fmp9hf9346h0kmdgqsqx0fhjr3k0wpw";
+        authors = [
+          "erick.tryzelaar <erick.tryzelaar@gmail.com>"
+          "kwantam <kwantam@gmail.com>"
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "utf8parse" = rec {
+        crateName = "utf8parse";
+        version = "0.2.1";
+        edition = "2018";
+        sha256 = "02ip1a0az0qmc2786vxk2nqwsgcwf17d3a38fkf0q7hrmwh9c6vi";
+        authors = [
+          "Joe Wilm <joe@jwilm.com>"
+          "Christian Duerr <contact@christianduerr.com>"
+        ];
+        features = { };
+        resolvedDefaultFeatures = [ "default" ];
+      };
+      "valuable" = rec {
+        crateName = "valuable";
+        version = "0.1.0";
+        edition = "2018";
+        sha256 = "0v9gp3nkjbl30z0fd56d8mx7w1csk86wwjhfjhr400wh9mfpw2w3";
+        features = {
+          "default" = [ "std" ];
+          "derive" = [ "valuable-derive" ];
+          "std" = [ "alloc" ];
+          "valuable-derive" = [ "dep:valuable-derive" ];
+        };
+        resolvedDefaultFeatures = [ "alloc" "std" ];
+      };
+      "version_check" = rec {
+        crateName = "version_check";
+        version = "0.9.4";
+        edition = "2015";
+        sha256 = "0gs8grwdlgh0xq660d7wr80x14vxbizmd8dbp29p2pdncx8lp1s9";
+        authors = [
+          "Sergio Benitez <sb@sergio.bz>"
+        ];
+
+      };
+      "wait-timeout" = rec {
+        crateName = "wait-timeout";
+        version = "0.2.0";
+        edition = "2015";
+        crateBin = [ ];
+        sha256 = "1xpkk0j5l9pfmjfh1pi0i89invlavfrd9av5xp0zhxgb29dhy84z";
+        authors = [
+          "Alex Crichton <alex@alexcrichton.com>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+            target = { target, features }: (target."unix" or false);
+          }
+        ];
+
+      };
+      "walkdir" = rec {
+        crateName = "walkdir";
+        version = "2.3.3";
+        edition = "2018";
+        sha256 = "16768hy32kcvghq7v4ci8llfjvdiwrwg6sj9nzcdiisnv9699prn";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "same-file";
+            packageId = "same-file";
+          }
+          {
+            name = "winapi-util";
+            packageId = "winapi-util";
+            target = { target, features }: (target."windows" or false);
+          }
+        ];
+
+      };
+      "want" = rec {
+        crateName = "want";
+        version = "0.3.0";
+        edition = "2018";
+        sha256 = "181b2zmwfq389x9n2g1n37cvcvvdand832zz6v8i1l8wrdlaks0w";
+        authors = [
+          "Sean McArthur <sean@seanmonstar.com>"
+        ];
+        dependencies = [
+          {
+            name = "log";
+            packageId = "log";
+          }
+          {
+            name = "try-lock";
+            packageId = "try-lock";
+          }
+        ];
+
+      };
+      "wasi" = rec {
+        crateName = "wasi";
+        version = "0.11.0+wasi-snapshot-preview1";
+        edition = "2018";
+        sha256 = "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw";
+        authors = [
+          "The Cranelift Project Developers"
+        ];
+        features = {
+          "compiler_builtins" = [ "dep:compiler_builtins" ];
+          "core" = [ "dep:core" ];
+          "default" = [ "std" ];
+          "rustc-dep-of-std" = [ "compiler_builtins" "core" "rustc-std-workspace-alloc" ];
+          "rustc-std-workspace-alloc" = [ "dep:rustc-std-workspace-alloc" ];
+        };
+        resolvedDefaultFeatures = [ "default" "std" ];
+      };
+      "wasm-bindgen" = rec {
+        crateName = "wasm-bindgen";
+        version = "0.2.84";
+        edition = "2018";
+        sha256 = "0fx5gh0b4n6znfa3blz92wn1k4bbiysyq9m95s7rn3gk46ydry1i";
+        authors = [
+          "The wasm-bindgen Developers"
+        ];
+        dependencies = [
+          {
+            name = "cfg-if";
+            packageId = "cfg-if";
+          }
+          {
+            name = "wasm-bindgen-macro";
+            packageId = "wasm-bindgen-macro";
+          }
+        ];
+        features = {
+          "default" = [ "spans" "std" ];
+          "enable-interning" = [ "std" ];
+          "serde" = [ "dep:serde" ];
+          "serde-serialize" = [ "serde" "serde_json" "std" ];
+          "serde_json" = [ "dep:serde_json" ];
+          "spans" = [ "wasm-bindgen-macro/spans" ];
+          "strict-macro" = [ "wasm-bindgen-macro/strict-macro" ];
+          "xxx_debug_only_print_generated_code" = [ "wasm-bindgen-macro/xxx_debug_only_print_generated_code" ];
+        };
+        resolvedDefaultFeatures = [ "default" "spans" "std" ];
+      };
+      "wasm-bindgen-backend" = rec {
+        crateName = "wasm-bindgen-backend";
+        version = "0.2.84";
+        edition = "2018";
+        sha256 = "1ffc0wb293ha56i66f830x7f8aa2xql69a21lrasy1ncbgyr1klm";
+        authors = [
+          "The wasm-bindgen Developers"
+        ];
+        dependencies = [
+          {
+            name = "bumpalo";
+            packageId = "bumpalo";
+          }
+          {
+            name = "log";
+            packageId = "log";
+          }
+          {
+            name = "once_cell";
+            packageId = "once_cell";
+          }
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "full" ];
+          }
+          {
+            name = "wasm-bindgen-shared";
+            packageId = "wasm-bindgen-shared";
+          }
+        ];
+        features = {
+          "extra-traits" = [ "syn/extra-traits" ];
+        };
+        resolvedDefaultFeatures = [ "spans" ];
+      };
+      "wasm-bindgen-macro" = rec {
+        crateName = "wasm-bindgen-macro";
+        version = "0.2.84";
+        edition = "2018";
+        sha256 = "1idlq28awqhq8rclb22rn5xix82w9a4rgy11vkapzhzd1dygf8ac";
+        procMacro = true;
+        authors = [
+          "The wasm-bindgen Developers"
+        ];
+        dependencies = [
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "wasm-bindgen-macro-support";
+            packageId = "wasm-bindgen-macro-support";
+          }
+        ];
+        features = {
+          "spans" = [ "wasm-bindgen-macro-support/spans" ];
+          "strict-macro" = [ "wasm-bindgen-macro-support/strict-macro" ];
+        };
+        resolvedDefaultFeatures = [ "spans" ];
+      };
+      "wasm-bindgen-macro-support" = rec {
+        crateName = "wasm-bindgen-macro-support";
+        version = "0.2.84";
+        edition = "2018";
+        sha256 = "1xm56lpi0rihh8ny7x085dgs3jdm47spgqflb98wghyadwq83zra";
+        authors = [
+          "The wasm-bindgen Developers"
+        ];
+        dependencies = [
+          {
+            name = "proc-macro2";
+            packageId = "proc-macro2 1.0.56";
+          }
+          {
+            name = "quote";
+            packageId = "quote 1.0.26";
+          }
+          {
+            name = "syn";
+            packageId = "syn 1.0.109";
+            features = [ "visit" "full" ];
+          }
+          {
+            name = "wasm-bindgen-backend";
+            packageId = "wasm-bindgen-backend";
+          }
+          {
+            name = "wasm-bindgen-shared";
+            packageId = "wasm-bindgen-shared";
+          }
+        ];
+        features = {
+          "extra-traits" = [ "syn/extra-traits" ];
+          "spans" = [ "wasm-bindgen-backend/spans" ];
+        };
+        resolvedDefaultFeatures = [ "spans" ];
+      };
+      "wasm-bindgen-shared" = rec {
+        crateName = "wasm-bindgen-shared";
+        version = "0.2.84";
+        edition = "2018";
+        sha256 = "0pcvk1c97r1pprzfaxxn359r0wqg5bm33ylbwgjh8f4cwbvzwih0";
+        authors = [
+          "The wasm-bindgen Developers"
+        ];
+
+      };
+      "web-sys" = rec {
+        crateName = "web-sys";
+        version = "0.3.61";
+        edition = "2018";
+        sha256 = "15qbbdbhyz02srvm01p0cvlh0pvmbbin9hislp0yx8rvnbs9jfz3";
+        authors = [
+          "The wasm-bindgen Developers"
+        ];
+        dependencies = [
+          {
+            name = "js-sys";
+            packageId = "js-sys";
+          }
+          {
+            name = "wasm-bindgen";
+            packageId = "wasm-bindgen";
+          }
+        ];
+        features = {
+          "AbortSignal" = [ "EventTarget" ];
+          "AnalyserNode" = [ "AudioNode" "EventTarget" ];
+          "Animation" = [ "EventTarget" ];
+          "AnimationEvent" = [ "Event" ];
+          "AnimationPlaybackEvent" = [ "Event" ];
+          "Attr" = [ "EventTarget" "Node" ];
+          "AudioBufferSourceNode" = [ "AudioNode" "AudioScheduledSourceNode" "EventTarget" ];
+          "AudioContext" = [ "BaseAudioContext" "EventTarget" ];
+          "AudioDestinationNode" = [ "AudioNode" "EventTarget" ];
+          "AudioNode" = [ "EventTarget" ];
+          "AudioProcessingEvent" = [ "Event" ];
+          "AudioScheduledSourceNode" = [ "AudioNode" "EventTarget" ];
+          "AudioStreamTrack" = [ "EventTarget" "MediaStreamTrack" ];
+          "AudioTrackList" = [ "EventTarget" ];
+          "AudioWorklet" = [ "Worklet" ];
+          "AudioWorkletGlobalScope" = [ "WorkletGlobalScope" ];
+          "AudioWorkletNode" = [ "AudioNode" "EventTarget" ];
+          "AuthenticatorAssertionResponse" = [ "AuthenticatorResponse" ];
+          "AuthenticatorAttestationResponse" = [ "AuthenticatorResponse" ];
+          "BaseAudioContext" = [ "EventTarget" ];
+          "BatteryManager" = [ "EventTarget" ];
+          "BeforeUnloadEvent" = [ "Event" ];
+          "BiquadFilterNode" = [ "AudioNode" "EventTarget" ];
+          "BlobEvent" = [ "Event" ];
+          "Bluetooth" = [ "EventTarget" ];
+          "BluetoothAdvertisingEvent" = [ "Event" ];
+          "BluetoothDevice" = [ "EventTarget" ];
+          "BluetoothPermissionResult" = [ "EventTarget" "PermissionStatus" ];
+          "BluetoothRemoteGattCharacteristic" = [ "EventTarget" ];
+          "BluetoothRemoteGattService" = [ "EventTarget" ];
+          "BroadcastChannel" = [ "EventTarget" ];
+          "CanvasCaptureMediaStream" = [ "EventTarget" "MediaStream" ];
+          "CdataSection" = [ "CharacterData" "EventTarget" "Node" "Text" ];
+          "ChannelMergerNode" = [ "AudioNode" "EventTarget" ];
+          "ChannelSplitterNode" = [ "AudioNode" "EventTarget" ];
+          "CharacterData" = [ "EventTarget" "Node" ];
+          "ChromeWorker" = [ "EventTarget" "Worker" ];
+          "Clipboard" = [ "EventTarget" ];
+          "ClipboardEvent" = [ "Event" ];
+          "CloseEvent" = [ "Event" ];
+          "Comment" = [ "CharacterData" "EventTarget" "Node" ];
+          "CompositionEvent" = [ "Event" "UiEvent" ];
+          "ConstantSourceNode" = [ "AudioNode" "AudioScheduledSourceNode" "EventTarget" ];
+          "ConvolverNode" = [ "AudioNode" "EventTarget" ];
+          "CssAnimation" = [ "Animation" "EventTarget" ];
+          "CssConditionRule" = [ "CssGroupingRule" "CssRule" ];
+          "CssCounterStyleRule" = [ "CssRule" ];
+          "CssFontFaceRule" = [ "CssRule" ];
+          "CssFontFeatureValuesRule" = [ "CssRule" ];
+          "CssGroupingRule" = [ "CssRule" ];
+          "CssImportRule" = [ "CssRule" ];
+          "CssKeyframeRule" = [ "CssRule" ];
+          "CssKeyframesRule" = [ "CssRule" ];
+          "CssMediaRule" = [ "CssConditionRule" "CssGroupingRule" "CssRule" ];
+          "CssNamespaceRule" = [ "CssRule" ];
+          "CssPageRule" = [ "CssRule" ];
+          "CssStyleRule" = [ "CssRule" ];
+          "CssStyleSheet" = [ "StyleSheet" ];
+          "CssSupportsRule" = [ "CssConditionRule" "CssGroupingRule" "CssRule" ];
+          "CssTransition" = [ "Animation" "EventTarget" ];
+          "CustomEvent" = [ "Event" ];
+          "DedicatedWorkerGlobalScope" = [ "EventTarget" "WorkerGlobalScope" ];
+          "DelayNode" = [ "AudioNode" "EventTarget" ];
+          "DeviceLightEvent" = [ "Event" ];
+          "DeviceMotionEvent" = [ "Event" ];
+          "DeviceOrientationEvent" = [ "Event" ];
+          "DeviceProximityEvent" = [ "Event" ];
+          "Document" = [ "EventTarget" "Node" ];
+          "DocumentFragment" = [ "EventTarget" "Node" ];
+          "DocumentTimeline" = [ "AnimationTimeline" ];
+          "DocumentType" = [ "EventTarget" "Node" ];
+          "DomMatrix" = [ "DomMatrixReadOnly" ];
+          "DomPoint" = [ "DomPointReadOnly" ];
+          "DomRect" = [ "DomRectReadOnly" ];
+          "DomRequest" = [ "EventTarget" ];
+          "DragEvent" = [ "Event" "MouseEvent" "UiEvent" ];
+          "DynamicsCompressorNode" = [ "AudioNode" "EventTarget" ];
+          "Element" = [ "EventTarget" "Node" ];
+          "ErrorEvent" = [ "Event" ];
+          "EventSource" = [ "EventTarget" ];
+          "ExtendableEvent" = [ "Event" ];
+          "ExtendableMessageEvent" = [ "Event" "ExtendableEvent" ];
+          "FetchEvent" = [ "Event" "ExtendableEvent" ];
+          "FetchObserver" = [ "EventTarget" ];
+          "File" = [ "Blob" ];
+          "FileReader" = [ "EventTarget" ];
+          "FileSystemDirectoryEntry" = [ "FileSystemEntry" ];
+          "FileSystemFileEntry" = [ "FileSystemEntry" ];
+          "FocusEvent" = [ "Event" "UiEvent" ];
+          "FontFaceSet" = [ "EventTarget" ];
+          "FontFaceSetLoadEvent" = [ "Event" ];
+          "GainNode" = [ "AudioNode" "EventTarget" ];
+          "GamepadAxisMoveEvent" = [ "Event" "GamepadEvent" ];
+          "GamepadButtonEvent" = [ "Event" "GamepadEvent" ];
+          "GamepadEvent" = [ "Event" ];
+          "GpuDevice" = [ "EventTarget" ];
+          "GpuInternalError" = [ "GpuError" ];
+          "GpuOutOfMemoryError" = [ "GpuError" ];
+          "GpuUncapturedErrorEvent" = [ "Event" ];
+          "GpuValidationError" = [ "GpuError" ];
+          "HashChangeEvent" = [ "Event" ];
+          "Hid" = [ "EventTarget" ];
+          "HidConnectionEvent" = [ "Event" ];
+          "HidDevice" = [ "EventTarget" ];
+          "HidInputReportEvent" = [ "Event" ];
+          "HtmlAnchorElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlAreaElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlAudioElement" = [ "Element" "EventTarget" "HtmlElement" "HtmlMediaElement" "Node" ];
+          "HtmlBaseElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlBodyElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlBrElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlButtonElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlCanvasElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDListElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDataElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDataListElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDetailsElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDialogElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDirectoryElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDivElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlDocument" = [ "Document" "EventTarget" "Node" ];
+          "HtmlElement" = [ "Element" "EventTarget" "Node" ];
+          "HtmlEmbedElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlFieldSetElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlFontElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlFormControlsCollection" = [ "HtmlCollection" ];
+          "HtmlFormElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlFrameElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlFrameSetElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlHeadElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlHeadingElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlHrElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlHtmlElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlIFrameElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlImageElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlInputElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlLabelElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlLegendElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlLiElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlLinkElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlMapElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlMediaElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlMenuElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlMenuItemElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlMetaElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlMeterElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlModElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlOListElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlObjectElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlOptGroupElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlOptionElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlOptionsCollection" = [ "HtmlCollection" ];
+          "HtmlOutputElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlParagraphElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlParamElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlPictureElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlPreElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlProgressElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlQuoteElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlScriptElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlSelectElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlSlotElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlSourceElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlSpanElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlStyleElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTableCaptionElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTableCellElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTableColElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTableElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTableRowElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTableSectionElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTemplateElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTextAreaElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTimeElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTitleElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlTrackElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlUListElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlUnknownElement" = [ "Element" "EventTarget" "HtmlElement" "Node" ];
+          "HtmlVideoElement" = [ "Element" "EventTarget" "HtmlElement" "HtmlMediaElement" "Node" ];
+          "IdbCursorWithValue" = [ "IdbCursor" ];
+          "IdbDatabase" = [ "EventTarget" ];
+          "IdbFileHandle" = [ "EventTarget" ];
+          "IdbFileRequest" = [ "DomRequest" "EventTarget" ];
+          "IdbLocaleAwareKeyRange" = [ "IdbKeyRange" ];
+          "IdbMutableFile" = [ "EventTarget" ];
+          "IdbOpenDbRequest" = [ "EventTarget" "IdbRequest" ];
+          "IdbRequest" = [ "EventTarget" ];
+          "IdbTransaction" = [ "EventTarget" ];
+          "IdbVersionChangeEvent" = [ "Event" ];
+          "IirFilterNode" = [ "AudioNode" "EventTarget" ];
+          "ImageCaptureErrorEvent" = [ "Event" ];
+          "ImageTrack" = [ "EventTarget" ];
+          "InputEvent" = [ "Event" "UiEvent" ];
+          "KeyboardEvent" = [ "Event" "UiEvent" ];
+          "KeyframeEffect" = [ "AnimationEffect" ];
+          "LocalMediaStream" = [ "EventTarget" "MediaStream" ];
+          "MediaDevices" = [ "EventTarget" ];
+          "MediaElementAudioSourceNode" = [ "AudioNode" "EventTarget" ];
+          "MediaEncryptedEvent" = [ "Event" ];
+          "MediaKeyError" = [ "Event" ];
+          "MediaKeyMessageEvent" = [ "Event" ];
+          "MediaKeySession" = [ "EventTarget" ];
+          "MediaQueryList" = [ "EventTarget" ];
+          "MediaQueryListEvent" = [ "Event" ];
+          "MediaRecorder" = [ "EventTarget" ];
+          "MediaRecorderErrorEvent" = [ "Event" ];
+          "MediaSource" = [ "EventTarget" ];
+          "MediaStream" = [ "EventTarget" ];
+          "MediaStreamAudioDestinationNode" = [ "AudioNode" "EventTarget" ];
+          "MediaStreamAudioSourceNode" = [ "AudioNode" "EventTarget" ];
+          "MediaStreamEvent" = [ "Event" ];
+          "MediaStreamTrack" = [ "EventTarget" ];
+          "MediaStreamTrackEvent" = [ "Event" ];
+          "MediaStreamTrackGenerator" = [ "EventTarget" "MediaStreamTrack" ];
+          "MessageEvent" = [ "Event" ];
+          "MessagePort" = [ "EventTarget" ];
+          "MidiAccess" = [ "EventTarget" ];
+          "MidiConnectionEvent" = [ "Event" ];
+          "MidiInput" = [ "EventTarget" "MidiPort" ];
+          "MidiMessageEvent" = [ "Event" ];
+          "MidiOutput" = [ "EventTarget" "MidiPort" ];
+          "MidiPort" = [ "EventTarget" ];
+          "MouseEvent" = [ "Event" "UiEvent" ];
+          "MouseScrollEvent" = [ "Event" "MouseEvent" "UiEvent" ];
+          "MutationEvent" = [ "Event" ];
+          "NetworkInformation" = [ "EventTarget" ];
+          "Node" = [ "EventTarget" ];
+          "Notification" = [ "EventTarget" ];
+          "NotificationEvent" = [ "Event" "ExtendableEvent" ];
+          "OfflineAudioCompletionEvent" = [ "Event" ];
+          "OfflineAudioContext" = [ "BaseAudioContext" "EventTarget" ];
+          "OfflineResourceList" = [ "EventTarget" ];
+          "OffscreenCanvas" = [ "EventTarget" ];
+          "OscillatorNode" = [ "AudioNode" "AudioScheduledSourceNode" "EventTarget" ];
+          "PageTransitionEvent" = [ "Event" ];
+          "PaintWorkletGlobalScope" = [ "WorkletGlobalScope" ];
+          "PannerNode" = [ "AudioNode" "EventTarget" ];
+          "PaymentMethodChangeEvent" = [ "Event" "PaymentRequestUpdateEvent" ];
+          "PaymentRequestUpdateEvent" = [ "Event" ];
+          "Performance" = [ "EventTarget" ];
+          "PerformanceMark" = [ "PerformanceEntry" ];
+          "PerformanceMeasure" = [ "PerformanceEntry" ];
+          "PerformanceNavigationTiming" = [ "PerformanceEntry" "PerformanceResourceTiming" ];
+          "PerformanceResourceTiming" = [ "PerformanceEntry" ];
+          "PermissionStatus" = [ "EventTarget" ];
+          "PointerEvent" = [ "Event" "MouseEvent" "UiEvent" ];
+          "PopStateEvent" = [ "Event" ];
+          "PopupBlockedEvent" = [ "Event" ];
+          "PresentationAvailability" = [ "EventTarget" ];
+          "PresentationConnection" = [ "EventTarget" ];
+          "PresentationConnectionAvailableEvent" = [ "Event" ];
+          "PresentationConnectionCloseEvent" = [ "Event" ];
+          "PresentationConnectionList" = [ "EventTarget" ];
+          "PresentationRequest" = [ "EventTarget" ];
+          "ProcessingInstruction" = [ "CharacterData" "EventTarget" "Node" ];
+          "ProgressEvent" = [ "Event" ];
+          "PromiseRejectionEvent" = [ "Event" ];
+          "PublicKeyCredential" = [ "Credential" ];
+          "PushEvent" = [ "Event" "ExtendableEvent" ];
+          "RadioNodeList" = [ "NodeList" ];
+          "RtcDataChannel" = [ "EventTarget" ];
+          "RtcDataChannelEvent" = [ "Event" ];
+          "RtcPeerConnection" = [ "EventTarget" ];
+          "RtcPeerConnectionIceEvent" = [ "Event" ];
+          "RtcTrackEvent" = [ "Event" ];
+          "RtcdtmfSender" = [ "EventTarget" ];
+          "RtcdtmfToneChangeEvent" = [ "Event" ];
+          "Screen" = [ "EventTarget" ];
+          "ScreenOrientation" = [ "EventTarget" ];
+          "ScriptProcessorNode" = [ "AudioNode" "EventTarget" ];
+          "ScrollAreaEvent" = [ "Event" "UiEvent" ];
+          "SecurityPolicyViolationEvent" = [ "Event" ];
+          "Serial" = [ "EventTarget" ];
+          "SerialPort" = [ "EventTarget" ];
+          "ServiceWorker" = [ "EventTarget" ];
+          "ServiceWorkerContainer" = [ "EventTarget" ];
+          "ServiceWorkerGlobalScope" = [ "EventTarget" "WorkerGlobalScope" ];
+          "ServiceWorkerRegistration" = [ "EventTarget" ];
+          "ShadowRoot" = [ "DocumentFragment" "EventTarget" "Node" ];
+          "SharedWorker" = [ "EventTarget" ];
+          "SharedWorkerGlobalScope" = [ "EventTarget" "WorkerGlobalScope" ];
+          "SourceBuffer" = [ "EventTarget" ];
+          "SourceBufferList" = [ "EventTarget" ];
+          "SpeechRecognition" = [ "EventTarget" ];
+          "SpeechRecognitionError" = [ "Event" ];
+          "SpeechRecognitionEvent" = [ "Event" ];
+          "SpeechSynthesis" = [ "EventTarget" ];
+          "SpeechSynthesisErrorEvent" = [ "Event" "SpeechSynthesisEvent" ];
+          "SpeechSynthesisEvent" = [ "Event" ];
+          "SpeechSynthesisUtterance" = [ "EventTarget" ];
+          "StereoPannerNode" = [ "AudioNode" "EventTarget" ];
+          "StorageEvent" = [ "Event" ];
+          "SubmitEvent" = [ "Event" ];
+          "SvgAnimateElement" = [ "Element" "EventTarget" "Node" "SvgAnimationElement" "SvgElement" ];
+          "SvgAnimateMotionElement" = [ "Element" "EventTarget" "Node" "SvgAnimationElement" "SvgElement" ];
+          "SvgAnimateTransformElement" = [ "Element" "EventTarget" "Node" "SvgAnimationElement" "SvgElement" ];
+          "SvgAnimationElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgCircleElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGeometryElement" "SvgGraphicsElement" ];
+          "SvgClipPathElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgComponentTransferFunctionElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgDefsElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgDescElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgElement" = [ "Element" "EventTarget" "Node" ];
+          "SvgEllipseElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGeometryElement" "SvgGraphicsElement" ];
+          "SvgFilterElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgForeignObjectElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgGeometryElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgGradientElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgGraphicsElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgImageElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgLineElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGeometryElement" "SvgGraphicsElement" ];
+          "SvgLinearGradientElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGradientElement" ];
+          "SvgMarkerElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgMaskElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgMetadataElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgPathElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGeometryElement" "SvgGraphicsElement" ];
+          "SvgPathSegArcAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegArcRel" = [ "SvgPathSeg" ];
+          "SvgPathSegClosePath" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoCubicAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoCubicRel" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoCubicSmoothAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoCubicSmoothRel" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoQuadraticAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoQuadraticRel" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoQuadraticSmoothAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegCurvetoQuadraticSmoothRel" = [ "SvgPathSeg" ];
+          "SvgPathSegLinetoAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegLinetoHorizontalAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegLinetoHorizontalRel" = [ "SvgPathSeg" ];
+          "SvgPathSegLinetoRel" = [ "SvgPathSeg" ];
+          "SvgPathSegLinetoVerticalAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegLinetoVerticalRel" = [ "SvgPathSeg" ];
+          "SvgPathSegMovetoAbs" = [ "SvgPathSeg" ];
+          "SvgPathSegMovetoRel" = [ "SvgPathSeg" ];
+          "SvgPatternElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgPolygonElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGeometryElement" "SvgGraphicsElement" ];
+          "SvgPolylineElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGeometryElement" "SvgGraphicsElement" ];
+          "SvgRadialGradientElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGradientElement" ];
+          "SvgRectElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGeometryElement" "SvgGraphicsElement" ];
+          "SvgScriptElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgSetElement" = [ "Element" "EventTarget" "Node" "SvgAnimationElement" "SvgElement" ];
+          "SvgStopElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgStyleElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgSwitchElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgSymbolElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgTextContentElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgTextElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" "SvgTextContentElement" "SvgTextPositioningElement" ];
+          "SvgTextPathElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" "SvgTextContentElement" ];
+          "SvgTextPositioningElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" "SvgTextContentElement" ];
+          "SvgTitleElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgUseElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgViewElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgaElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgfeBlendElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeColorMatrixElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeComponentTransferElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeCompositeElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeConvolveMatrixElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeDiffuseLightingElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeDisplacementMapElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeDistantLightElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeDropShadowElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeFloodElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeFuncAElement" = [ "Element" "EventTarget" "Node" "SvgComponentTransferFunctionElement" "SvgElement" ];
+          "SvgfeFuncBElement" = [ "Element" "EventTarget" "Node" "SvgComponentTransferFunctionElement" "SvgElement" ];
+          "SvgfeFuncGElement" = [ "Element" "EventTarget" "Node" "SvgComponentTransferFunctionElement" "SvgElement" ];
+          "SvgfeFuncRElement" = [ "Element" "EventTarget" "Node" "SvgComponentTransferFunctionElement" "SvgElement" ];
+          "SvgfeGaussianBlurElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeImageElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeMergeElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeMergeNodeElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeMorphologyElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeOffsetElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfePointLightElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeSpecularLightingElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeSpotLightElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeTileElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgfeTurbulenceElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvggElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgmPathElement" = [ "Element" "EventTarget" "Node" "SvgElement" ];
+          "SvgsvgElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" ];
+          "SvgtSpanElement" = [ "Element" "EventTarget" "Node" "SvgElement" "SvgGraphicsElement" "SvgTextContentElement" "SvgTextPositioningElement" ];
+          "TcpServerSocket" = [ "EventTarget" ];
+          "TcpServerSocketEvent" = [ "Event" ];
+          "TcpSocket" = [ "EventTarget" ];
+          "TcpSocketErrorEvent" = [ "Event" ];
+          "TcpSocketEvent" = [ "Event" ];
+          "Text" = [ "CharacterData" "EventTarget" "Node" ];
+          "TextTrack" = [ "EventTarget" ];
+          "TextTrackCue" = [ "EventTarget" ];
+          "TextTrackList" = [ "EventTarget" ];
+          "TimeEvent" = [ "Event" ];
+          "TouchEvent" = [ "Event" "UiEvent" ];
+          "TrackEvent" = [ "Event" ];
+          "TransitionEvent" = [ "Event" ];
+          "UiEvent" = [ "Event" ];
+          "Usb" = [ "EventTarget" ];
+          "UsbConnectionEvent" = [ "Event" ];
+          "UsbPermissionResult" = [ "EventTarget" "PermissionStatus" ];
+          "UserProximityEvent" = [ "Event" ];
+          "ValueEvent" = [ "Event" ];
+          "VideoStreamTrack" = [ "EventTarget" "MediaStreamTrack" ];
+          "VideoTrackList" = [ "EventTarget" ];
+          "VrDisplay" = [ "EventTarget" ];
+          "VttCue" = [ "EventTarget" "TextTrackCue" ];
+          "WakeLockSentinel" = [ "EventTarget" ];
+          "WaveShaperNode" = [ "AudioNode" "EventTarget" ];
+          "WebGlContextEvent" = [ "Event" ];
+          "WebKitCssMatrix" = [ "DomMatrix" "DomMatrixReadOnly" ];
+          "WebSocket" = [ "EventTarget" ];
+          "WheelEvent" = [ "Event" "MouseEvent" "UiEvent" ];
+          "Window" = [ "EventTarget" ];
+          "WindowClient" = [ "Client" ];
+          "Worker" = [ "EventTarget" ];
+          "WorkerDebuggerGlobalScope" = [ "EventTarget" ];
+          "WorkerGlobalScope" = [ "EventTarget" ];
+          "XmlDocument" = [ "Document" "EventTarget" "Node" ];
+          "XmlHttpRequest" = [ "EventTarget" "XmlHttpRequestEventTarget" ];
+          "XmlHttpRequestEventTarget" = [ "EventTarget" ];
+          "XmlHttpRequestUpload" = [ "EventTarget" "XmlHttpRequestEventTarget" ];
+          "XrBoundedReferenceSpace" = [ "EventTarget" "XrReferenceSpace" "XrSpace" ];
+          "XrHand" = [ "XrHandJoint" ];
+          "XrInputSourceEvent" = [ "Event" ];
+          "XrInputSourcesChangeEvent" = [ "Event" ];
+          "XrLayer" = [ "EventTarget" ];
+          "XrPermissionStatus" = [ "EventTarget" "PermissionStatus" ];
+          "XrReferenceSpace" = [ "EventTarget" "XrSpace" ];
+          "XrReferenceSpaceEvent" = [ "Event" ];
+          "XrSession" = [ "EventTarget" ];
+          "XrSessionEvent" = [ "Event" ];
+          "XrSpace" = [ "EventTarget" ];
+          "XrSystem" = [ "EventTarget" ];
+          "XrViewerPose" = [ "XrPose" ];
+          "XrWebGlLayer" = [ "EventTarget" "XrLayer" ];
+        };
+        resolvedDefaultFeatures = [ "CanvasRenderingContext2d" "Document" "DomRect" "DomRectReadOnly" "Element" "EventTarget" "HtmlCanvasElement" "HtmlElement" "Node" "Window" ];
+      };
+      "which" = rec {
+        crateName = "which";
+        version = "4.4.0";
+        edition = "2018";
+        sha256 = "0sd24r17q4j3hc2yjjrg9q4qya1y4n9zq0bj9c2rla1bqn2cfh94";
+        authors = [
+          "Harry Fei <tiziyuanfang@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "either";
+            packageId = "either";
+          }
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+          {
+            name = "once_cell";
+            packageId = "once_cell";
+            target = { target, features }: (target."windows" or false);
+          }
+        ];
+        features = {
+          "regex" = [ "dep:regex" ];
+        };
+      };
+      "winapi" = rec {
+        crateName = "winapi";
+        version = "0.3.9";
+        edition = "2015";
+        sha256 = "06gl025x418lchw1wxj64ycr7gha83m44cjr5sarhynd9xkrm0sw";
+        authors = [
+          "Peter Atashian <retep998@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "winapi-i686-pc-windows-gnu";
+            packageId = "winapi-i686-pc-windows-gnu";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "i686-pc-windows-gnu");
+          }
+          {
+            name = "winapi-x86_64-pc-windows-gnu";
+            packageId = "winapi-x86_64-pc-windows-gnu";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "x86_64-pc-windows-gnu");
+          }
+        ];
+        features = {
+          "debug" = [ "impl-debug" ];
+        };
+        resolvedDefaultFeatures = [ "basetsd" "consoleapi" "errhandlingapi" "fileapi" "handleapi" "knownfolders" "minwinbase" "minwindef" "ntsecapi" "ntstatus" "objbase" "processenv" "processthreadsapi" "profileapi" "shellapi" "shlobj" "std" "stringapiset" "synchapi" "winbase" "wincon" "winerror" "winnt" "winuser" "ws2ipdef" "ws2tcpip" ];
+      };
+      "winapi-i686-pc-windows-gnu" = rec {
+        crateName = "winapi-i686-pc-windows-gnu";
+        version = "0.4.0";
+        edition = "2015";
+        sha256 = "1dmpa6mvcvzz16zg6d5vrfy4bxgg541wxrcip7cnshi06v38ffxc";
+        authors = [
+          "Peter Atashian <retep998@gmail.com>"
+        ];
+
+      };
+      "winapi-util" = rec {
+        crateName = "winapi-util";
+        version = "0.1.5";
+        edition = "2018";
+        sha256 = "0y71bp7f6d536czj40dhqk0d55wfbbwqfp2ymqf1an5ibgl6rv3h";
+        authors = [
+          "Andrew Gallant <jamslam@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "winapi";
+            packageId = "winapi";
+            target = { target, features }: (target."windows" or false);
+            features = [ "std" "consoleapi" "errhandlingapi" "fileapi" "minwindef" "processenv" "winbase" "wincon" "winerror" "winnt" ];
+          }
+        ];
+
+      };
+      "winapi-x86_64-pc-windows-gnu" = rec {
+        crateName = "winapi-x86_64-pc-windows-gnu";
+        version = "0.4.0";
+        edition = "2015";
+        sha256 = "0gqq64czqb64kskjryj8isp62m2sgvx25yyj3kpc2myh85w24bki";
+        authors = [
+          "Peter Atashian <retep998@gmail.com>"
+        ];
+
+      };
+      "windows-sys 0.45.0" = rec {
+        crateName = "windows-sys";
+        version = "0.45.0";
+        edition = "2018";
+        sha256 = "1l36bcqm4g89pknfp8r9rl1w4bn017q6a8qlx8viv0xjxzjkna3m";
+        authors = [
+          "Microsoft"
+        ];
+        dependencies = [
+          {
+            name = "windows-targets";
+            packageId = "windows-targets 0.42.2";
+            target = { target, features }: (!(target."windows_raw_dylib" or false));
+          }
+        ];
+        features = {
+          "Win32_Data" = [ "Win32" ];
+          "Win32_Data_HtmlHelp" = [ "Win32_Data" ];
+          "Win32_Data_RightsManagement" = [ "Win32_Data" ];
+          "Win32_Data_Xml" = [ "Win32_Data" ];
+          "Win32_Data_Xml_MsXml" = [ "Win32_Data_Xml" ];
+          "Win32_Data_Xml_XmlLite" = [ "Win32_Data_Xml" ];
+          "Win32_Devices" = [ "Win32" ];
+          "Win32_Devices_AllJoyn" = [ "Win32_Devices" ];
+          "Win32_Devices_BiometricFramework" = [ "Win32_Devices" ];
+          "Win32_Devices_Bluetooth" = [ "Win32_Devices" ];
+          "Win32_Devices_Communication" = [ "Win32_Devices" ];
+          "Win32_Devices_DeviceAccess" = [ "Win32_Devices" ];
+          "Win32_Devices_DeviceAndDriverInstallation" = [ "Win32_Devices" ];
+          "Win32_Devices_DeviceQuery" = [ "Win32_Devices" ];
+          "Win32_Devices_Display" = [ "Win32_Devices" ];
+          "Win32_Devices_Enumeration" = [ "Win32_Devices" ];
+          "Win32_Devices_Enumeration_Pnp" = [ "Win32_Devices_Enumeration" ];
+          "Win32_Devices_Fax" = [ "Win32_Devices" ];
+          "Win32_Devices_FunctionDiscovery" = [ "Win32_Devices" ];
+          "Win32_Devices_Geolocation" = [ "Win32_Devices" ];
+          "Win32_Devices_HumanInterfaceDevice" = [ "Win32_Devices" ];
+          "Win32_Devices_ImageAcquisition" = [ "Win32_Devices" ];
+          "Win32_Devices_PortableDevices" = [ "Win32_Devices" ];
+          "Win32_Devices_Properties" = [ "Win32_Devices" ];
+          "Win32_Devices_Pwm" = [ "Win32_Devices" ];
+          "Win32_Devices_Sensors" = [ "Win32_Devices" ];
+          "Win32_Devices_SerialCommunication" = [ "Win32_Devices" ];
+          "Win32_Devices_Tapi" = [ "Win32_Devices" ];
+          "Win32_Devices_Usb" = [ "Win32_Devices" ];
+          "Win32_Devices_WebServicesOnDevices" = [ "Win32_Devices" ];
+          "Win32_Foundation" = [ "Win32" ];
+          "Win32_Gaming" = [ "Win32" ];
+          "Win32_Globalization" = [ "Win32" ];
+          "Win32_Graphics" = [ "Win32" ];
+          "Win32_Graphics_Dwm" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Gdi" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Hlsl" = [ "Win32_Graphics" ];
+          "Win32_Graphics_OpenGL" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Printing" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Printing_PrintTicket" = [ "Win32_Graphics_Printing" ];
+          "Win32_Management" = [ "Win32" ];
+          "Win32_Management_MobileDeviceManagementRegistration" = [ "Win32_Management" ];
+          "Win32_Media" = [ "Win32" ];
+          "Win32_Media_Audio" = [ "Win32_Media" ];
+          "Win32_Media_Audio_Apo" = [ "Win32_Media_Audio" ];
+          "Win32_Media_Audio_DirectMusic" = [ "Win32_Media_Audio" ];
+          "Win32_Media_Audio_Endpoints" = [ "Win32_Media_Audio" ];
+          "Win32_Media_Audio_XAudio2" = [ "Win32_Media_Audio" ];
+          "Win32_Media_DeviceManager" = [ "Win32_Media" ];
+          "Win32_Media_DxMediaObjects" = [ "Win32_Media" ];
+          "Win32_Media_KernelStreaming" = [ "Win32_Media" ];
+          "Win32_Media_LibrarySharingServices" = [ "Win32_Media" ];
+          "Win32_Media_MediaPlayer" = [ "Win32_Media" ];
+          "Win32_Media_Multimedia" = [ "Win32_Media" ];
+          "Win32_Media_Speech" = [ "Win32_Media" ];
+          "Win32_Media_Streaming" = [ "Win32_Media" ];
+          "Win32_Media_WindowsMediaFormat" = [ "Win32_Media" ];
+          "Win32_NetworkManagement" = [ "Win32" ];
+          "Win32_NetworkManagement_Dhcp" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Dns" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_InternetConnectionWizard" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_IpHelper" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_MobileBroadband" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Multicast" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Ndis" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetBios" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetManagement" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetShell" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetworkDiagnosticsFramework" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetworkPolicyServer" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_P2P" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_QoS" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Rras" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Snmp" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WNet" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WebDav" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WiFi" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsConnectNow" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsConnectionManager" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsFilteringPlatform" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsFirewall" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsNetworkVirtualization" = [ "Win32_NetworkManagement" ];
+          "Win32_Networking" = [ "Win32" ];
+          "Win32_Networking_ActiveDirectory" = [ "Win32_Networking" ];
+          "Win32_Networking_BackgroundIntelligentTransferService" = [ "Win32_Networking" ];
+          "Win32_Networking_Clustering" = [ "Win32_Networking" ];
+          "Win32_Networking_HttpServer" = [ "Win32_Networking" ];
+          "Win32_Networking_Ldap" = [ "Win32_Networking" ];
+          "Win32_Networking_NetworkListManager" = [ "Win32_Networking" ];
+          "Win32_Networking_RemoteDifferentialCompression" = [ "Win32_Networking" ];
+          "Win32_Networking_WebSocket" = [ "Win32_Networking" ];
+          "Win32_Networking_WinHttp" = [ "Win32_Networking" ];
+          "Win32_Networking_WinInet" = [ "Win32_Networking" ];
+          "Win32_Networking_WinSock" = [ "Win32_Networking" ];
+          "Win32_Networking_WindowsWebServices" = [ "Win32_Networking" ];
+          "Win32_Security" = [ "Win32" ];
+          "Win32_Security_AppLocker" = [ "Win32_Security" ];
+          "Win32_Security_Authentication" = [ "Win32_Security" ];
+          "Win32_Security_Authentication_Identity" = [ "Win32_Security_Authentication" ];
+          "Win32_Security_Authentication_Identity_Provider" = [ "Win32_Security_Authentication_Identity" ];
+          "Win32_Security_Authorization" = [ "Win32_Security" ];
+          "Win32_Security_Authorization_UI" = [ "Win32_Security_Authorization" ];
+          "Win32_Security_ConfigurationSnapin" = [ "Win32_Security" ];
+          "Win32_Security_Credentials" = [ "Win32_Security" ];
+          "Win32_Security_Cryptography" = [ "Win32_Security" ];
+          "Win32_Security_Cryptography_Catalog" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_Cryptography_Certificates" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_Cryptography_Sip" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_Cryptography_UI" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_DiagnosticDataQuery" = [ "Win32_Security" ];
+          "Win32_Security_DirectoryServices" = [ "Win32_Security" ];
+          "Win32_Security_EnterpriseData" = [ "Win32_Security" ];
+          "Win32_Security_ExtensibleAuthenticationProtocol" = [ "Win32_Security" ];
+          "Win32_Security_Isolation" = [ "Win32_Security" ];
+          "Win32_Security_LicenseProtection" = [ "Win32_Security" ];
+          "Win32_Security_NetworkAccessProtection" = [ "Win32_Security" ];
+          "Win32_Security_Tpm" = [ "Win32_Security" ];
+          "Win32_Security_WinTrust" = [ "Win32_Security" ];
+          "Win32_Security_WinWlx" = [ "Win32_Security" ];
+          "Win32_Storage" = [ "Win32" ];
+          "Win32_Storage_Cabinets" = [ "Win32_Storage" ];
+          "Win32_Storage_CloudFilters" = [ "Win32_Storage" ];
+          "Win32_Storage_Compression" = [ "Win32_Storage" ];
+          "Win32_Storage_DataDeduplication" = [ "Win32_Storage" ];
+          "Win32_Storage_DistributedFileSystem" = [ "Win32_Storage" ];
+          "Win32_Storage_EnhancedStorage" = [ "Win32_Storage" ];
+          "Win32_Storage_FileHistory" = [ "Win32_Storage" ];
+          "Win32_Storage_FileServerResourceManager" = [ "Win32_Storage" ];
+          "Win32_Storage_FileSystem" = [ "Win32_Storage" ];
+          "Win32_Storage_Imapi" = [ "Win32_Storage" ];
+          "Win32_Storage_IndexServer" = [ "Win32_Storage" ];
+          "Win32_Storage_InstallableFileSystems" = [ "Win32_Storage" ];
+          "Win32_Storage_IscsiDisc" = [ "Win32_Storage" ];
+          "Win32_Storage_Jet" = [ "Win32_Storage" ];
+          "Win32_Storage_OfflineFiles" = [ "Win32_Storage" ];
+          "Win32_Storage_OperationRecorder" = [ "Win32_Storage" ];
+          "Win32_Storage_Packaging" = [ "Win32_Storage" ];
+          "Win32_Storage_Packaging_Appx" = [ "Win32_Storage_Packaging" ];
+          "Win32_Storage_Packaging_Opc" = [ "Win32_Storage_Packaging" ];
+          "Win32_Storage_ProjectedFileSystem" = [ "Win32_Storage" ];
+          "Win32_Storage_StructuredStorage" = [ "Win32_Storage" ];
+          "Win32_Storage_Vhd" = [ "Win32_Storage" ];
+          "Win32_Storage_VirtualDiskService" = [ "Win32_Storage" ];
+          "Win32_Storage_Vss" = [ "Win32_Storage" ];
+          "Win32_Storage_Xps" = [ "Win32_Storage" ];
+          "Win32_Storage_Xps_Printing" = [ "Win32_Storage_Xps" ];
+          "Win32_System" = [ "Win32" ];
+          "Win32_System_AddressBook" = [ "Win32_System" ];
+          "Win32_System_Antimalware" = [ "Win32_System" ];
+          "Win32_System_ApplicationInstallationAndServicing" = [ "Win32_System" ];
+          "Win32_System_ApplicationVerifier" = [ "Win32_System" ];
+          "Win32_System_AssessmentTool" = [ "Win32_System" ];
+          "Win32_System_Com" = [ "Win32_System" ];
+          "Win32_System_Com_CallObj" = [ "Win32_System_Com" ];
+          "Win32_System_Com_ChannelCredentials" = [ "Win32_System_Com" ];
+          "Win32_System_Com_Events" = [ "Win32_System_Com" ];
+          "Win32_System_Com_Marshal" = [ "Win32_System_Com" ];
+          "Win32_System_Com_StructuredStorage" = [ "Win32_System_Com" ];
+          "Win32_System_Com_UI" = [ "Win32_System_Com" ];
+          "Win32_System_Com_Urlmon" = [ "Win32_System_Com" ];
+          "Win32_System_ComponentServices" = [ "Win32_System" ];
+          "Win32_System_Console" = [ "Win32_System" ];
+          "Win32_System_Contacts" = [ "Win32_System" ];
+          "Win32_System_CorrelationVector" = [ "Win32_System" ];
+          "Win32_System_DataExchange" = [ "Win32_System" ];
+          "Win32_System_DeploymentServices" = [ "Win32_System" ];
+          "Win32_System_DesktopSharing" = [ "Win32_System" ];
+          "Win32_System_DeveloperLicensing" = [ "Win32_System" ];
+          "Win32_System_Diagnostics" = [ "Win32_System" ];
+          "Win32_System_Diagnostics_Ceip" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_Debug" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_Etw" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_ProcessSnapshotting" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_ToolHelp" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_DistributedTransactionCoordinator" = [ "Win32_System" ];
+          "Win32_System_Environment" = [ "Win32_System" ];
+          "Win32_System_ErrorReporting" = [ "Win32_System" ];
+          "Win32_System_EventCollector" = [ "Win32_System" ];
+          "Win32_System_EventLog" = [ "Win32_System" ];
+          "Win32_System_EventNotificationService" = [ "Win32_System" ];
+          "Win32_System_GroupPolicy" = [ "Win32_System" ];
+          "Win32_System_HostCompute" = [ "Win32_System" ];
+          "Win32_System_HostComputeNetwork" = [ "Win32_System" ];
+          "Win32_System_HostComputeSystem" = [ "Win32_System" ];
+          "Win32_System_Hypervisor" = [ "Win32_System" ];
+          "Win32_System_IO" = [ "Win32_System" ];
+          "Win32_System_Iis" = [ "Win32_System" ];
+          "Win32_System_Ioctl" = [ "Win32_System" ];
+          "Win32_System_JobObjects" = [ "Win32_System" ];
+          "Win32_System_Js" = [ "Win32_System" ];
+          "Win32_System_Kernel" = [ "Win32_System" ];
+          "Win32_System_LibraryLoader" = [ "Win32_System" ];
+          "Win32_System_Mailslots" = [ "Win32_System" ];
+          "Win32_System_Mapi" = [ "Win32_System" ];
+          "Win32_System_Memory" = [ "Win32_System" ];
+          "Win32_System_Memory_NonVolatile" = [ "Win32_System_Memory" ];
+          "Win32_System_MessageQueuing" = [ "Win32_System" ];
+          "Win32_System_MixedReality" = [ "Win32_System" ];
+          "Win32_System_Mmc" = [ "Win32_System" ];
+          "Win32_System_Ole" = [ "Win32_System" ];
+          "Win32_System_ParentalControls" = [ "Win32_System" ];
+          "Win32_System_PasswordManagement" = [ "Win32_System" ];
+          "Win32_System_Performance" = [ "Win32_System" ];
+          "Win32_System_Performance_HardwareCounterProfiling" = [ "Win32_System_Performance" ];
+          "Win32_System_Pipes" = [ "Win32_System" ];
+          "Win32_System_Power" = [ "Win32_System" ];
+          "Win32_System_ProcessStatus" = [ "Win32_System" ];
+          "Win32_System_RealTimeCommunications" = [ "Win32_System" ];
+          "Win32_System_Recovery" = [ "Win32_System" ];
+          "Win32_System_Registry" = [ "Win32_System" ];
+          "Win32_System_RemoteAssistance" = [ "Win32_System" ];
+          "Win32_System_RemoteDesktop" = [ "Win32_System" ];
+          "Win32_System_RemoteManagement" = [ "Win32_System" ];
+          "Win32_System_RestartManager" = [ "Win32_System" ];
+          "Win32_System_Restore" = [ "Win32_System" ];
+          "Win32_System_Rpc" = [ "Win32_System" ];
+          "Win32_System_Search" = [ "Win32_System" ];
+          "Win32_System_Search_Common" = [ "Win32_System_Search" ];
+          "Win32_System_SecurityCenter" = [ "Win32_System" ];
+          "Win32_System_ServerBackup" = [ "Win32_System" ];
+          "Win32_System_Services" = [ "Win32_System" ];
+          "Win32_System_SettingsManagementInfrastructure" = [ "Win32_System" ];
+          "Win32_System_SetupAndMigration" = [ "Win32_System" ];
+          "Win32_System_Shutdown" = [ "Win32_System" ];
+          "Win32_System_StationsAndDesktops" = [ "Win32_System" ];
+          "Win32_System_SubsystemForLinux" = [ "Win32_System" ];
+          "Win32_System_SystemInformation" = [ "Win32_System" ];
+          "Win32_System_SystemServices" = [ "Win32_System" ];
+          "Win32_System_TaskScheduler" = [ "Win32_System" ];
+          "Win32_System_Threading" = [ "Win32_System" ];
+          "Win32_System_Time" = [ "Win32_System" ];
+          "Win32_System_TpmBaseServices" = [ "Win32_System" ];
+          "Win32_System_UpdateAgent" = [ "Win32_System" ];
+          "Win32_System_UpdateAssessment" = [ "Win32_System" ];
+          "Win32_System_UserAccessLogging" = [ "Win32_System" ];
+          "Win32_System_VirtualDosMachines" = [ "Win32_System" ];
+          "Win32_System_WindowsProgramming" = [ "Win32_System" ];
+          "Win32_System_WindowsSync" = [ "Win32_System" ];
+          "Win32_System_Wmi" = [ "Win32_System" ];
+          "Win32_UI" = [ "Win32" ];
+          "Win32_UI_Accessibility" = [ "Win32_UI" ];
+          "Win32_UI_Animation" = [ "Win32_UI" ];
+          "Win32_UI_ColorSystem" = [ "Win32_UI" ];
+          "Win32_UI_Controls" = [ "Win32_UI" ];
+          "Win32_UI_Controls_Dialogs" = [ "Win32_UI_Controls" ];
+          "Win32_UI_Controls_RichEdit" = [ "Win32_UI_Controls" ];
+          "Win32_UI_HiDpi" = [ "Win32_UI" ];
+          "Win32_UI_Input" = [ "Win32_UI" ];
+          "Win32_UI_Input_Ime" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Ink" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_KeyboardAndMouse" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Pointer" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Radial" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Touch" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_XboxController" = [ "Win32_UI_Input" ];
+          "Win32_UI_InteractionContext" = [ "Win32_UI" ];
+          "Win32_UI_LegacyWindowsEnvironmentFeatures" = [ "Win32_UI" ];
+          "Win32_UI_Magnification" = [ "Win32_UI" ];
+          "Win32_UI_Notifications" = [ "Win32_UI" ];
+          "Win32_UI_Ribbon" = [ "Win32_UI" ];
+          "Win32_UI_Shell" = [ "Win32_UI" ];
+          "Win32_UI_Shell_Common" = [ "Win32_UI_Shell" ];
+          "Win32_UI_Shell_PropertiesSystem" = [ "Win32_UI_Shell" ];
+          "Win32_UI_TabletPC" = [ "Win32_UI" ];
+          "Win32_UI_TextServices" = [ "Win32_UI" ];
+          "Win32_UI_WindowsAndMessaging" = [ "Win32_UI" ];
+          "Win32_UI_Wpf" = [ "Win32_UI" ];
+        };
+        resolvedDefaultFeatures = [ "Win32" "Win32_Foundation" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_IO" "Win32_System_Pipes" "Win32_System_WindowsProgramming" "default" ];
+      };
+      "windows-sys 0.48.0" = rec {
+        crateName = "windows-sys";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "1aan23v5gs7gya1lc46hqn9mdh8yph3fhxmhxlw36pn6pqc28zb7";
+        authors = [
+          "Microsoft"
+        ];
+        dependencies = [
+          {
+            name = "windows-targets";
+            packageId = "windows-targets 0.48.0";
+          }
+        ];
+        features = {
+          "Wdk_System" = [ "Wdk" ];
+          "Wdk_System_OfflineRegistry" = [ "Wdk_System" ];
+          "Win32_Data" = [ "Win32" ];
+          "Win32_Data_HtmlHelp" = [ "Win32_Data" ];
+          "Win32_Data_RightsManagement" = [ "Win32_Data" ];
+          "Win32_Data_Xml" = [ "Win32_Data" ];
+          "Win32_Data_Xml_MsXml" = [ "Win32_Data_Xml" ];
+          "Win32_Data_Xml_XmlLite" = [ "Win32_Data_Xml" ];
+          "Win32_Devices" = [ "Win32" ];
+          "Win32_Devices_AllJoyn" = [ "Win32_Devices" ];
+          "Win32_Devices_BiometricFramework" = [ "Win32_Devices" ];
+          "Win32_Devices_Bluetooth" = [ "Win32_Devices" ];
+          "Win32_Devices_Communication" = [ "Win32_Devices" ];
+          "Win32_Devices_DeviceAccess" = [ "Win32_Devices" ];
+          "Win32_Devices_DeviceAndDriverInstallation" = [ "Win32_Devices" ];
+          "Win32_Devices_DeviceQuery" = [ "Win32_Devices" ];
+          "Win32_Devices_Display" = [ "Win32_Devices" ];
+          "Win32_Devices_Enumeration" = [ "Win32_Devices" ];
+          "Win32_Devices_Enumeration_Pnp" = [ "Win32_Devices_Enumeration" ];
+          "Win32_Devices_Fax" = [ "Win32_Devices" ];
+          "Win32_Devices_FunctionDiscovery" = [ "Win32_Devices" ];
+          "Win32_Devices_Geolocation" = [ "Win32_Devices" ];
+          "Win32_Devices_HumanInterfaceDevice" = [ "Win32_Devices" ];
+          "Win32_Devices_ImageAcquisition" = [ "Win32_Devices" ];
+          "Win32_Devices_PortableDevices" = [ "Win32_Devices" ];
+          "Win32_Devices_Properties" = [ "Win32_Devices" ];
+          "Win32_Devices_Pwm" = [ "Win32_Devices" ];
+          "Win32_Devices_Sensors" = [ "Win32_Devices" ];
+          "Win32_Devices_SerialCommunication" = [ "Win32_Devices" ];
+          "Win32_Devices_Tapi" = [ "Win32_Devices" ];
+          "Win32_Devices_Usb" = [ "Win32_Devices" ];
+          "Win32_Devices_WebServicesOnDevices" = [ "Win32_Devices" ];
+          "Win32_Foundation" = [ "Win32" ];
+          "Win32_Gaming" = [ "Win32" ];
+          "Win32_Globalization" = [ "Win32" ];
+          "Win32_Graphics" = [ "Win32" ];
+          "Win32_Graphics_Dwm" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Gdi" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Hlsl" = [ "Win32_Graphics" ];
+          "Win32_Graphics_OpenGL" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Printing" = [ "Win32_Graphics" ];
+          "Win32_Graphics_Printing_PrintTicket" = [ "Win32_Graphics_Printing" ];
+          "Win32_Management" = [ "Win32" ];
+          "Win32_Management_MobileDeviceManagementRegistration" = [ "Win32_Management" ];
+          "Win32_Media" = [ "Win32" ];
+          "Win32_Media_Audio" = [ "Win32_Media" ];
+          "Win32_Media_Audio_Apo" = [ "Win32_Media_Audio" ];
+          "Win32_Media_Audio_DirectMusic" = [ "Win32_Media_Audio" ];
+          "Win32_Media_Audio_Endpoints" = [ "Win32_Media_Audio" ];
+          "Win32_Media_Audio_XAudio2" = [ "Win32_Media_Audio" ];
+          "Win32_Media_DeviceManager" = [ "Win32_Media" ];
+          "Win32_Media_DxMediaObjects" = [ "Win32_Media" ];
+          "Win32_Media_KernelStreaming" = [ "Win32_Media" ];
+          "Win32_Media_LibrarySharingServices" = [ "Win32_Media" ];
+          "Win32_Media_MediaPlayer" = [ "Win32_Media" ];
+          "Win32_Media_Multimedia" = [ "Win32_Media" ];
+          "Win32_Media_Speech" = [ "Win32_Media" ];
+          "Win32_Media_Streaming" = [ "Win32_Media" ];
+          "Win32_Media_WindowsMediaFormat" = [ "Win32_Media" ];
+          "Win32_NetworkManagement" = [ "Win32" ];
+          "Win32_NetworkManagement_Dhcp" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Dns" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_InternetConnectionWizard" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_IpHelper" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_MobileBroadband" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Multicast" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Ndis" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetBios" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetManagement" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetShell" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetworkDiagnosticsFramework" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_NetworkPolicyServer" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_P2P" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_QoS" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Rras" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_Snmp" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WNet" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WebDav" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WiFi" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsConnectNow" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsConnectionManager" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsFilteringPlatform" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsFirewall" = [ "Win32_NetworkManagement" ];
+          "Win32_NetworkManagement_WindowsNetworkVirtualization" = [ "Win32_NetworkManagement" ];
+          "Win32_Networking" = [ "Win32" ];
+          "Win32_Networking_ActiveDirectory" = [ "Win32_Networking" ];
+          "Win32_Networking_BackgroundIntelligentTransferService" = [ "Win32_Networking" ];
+          "Win32_Networking_Clustering" = [ "Win32_Networking" ];
+          "Win32_Networking_HttpServer" = [ "Win32_Networking" ];
+          "Win32_Networking_Ldap" = [ "Win32_Networking" ];
+          "Win32_Networking_NetworkListManager" = [ "Win32_Networking" ];
+          "Win32_Networking_RemoteDifferentialCompression" = [ "Win32_Networking" ];
+          "Win32_Networking_WebSocket" = [ "Win32_Networking" ];
+          "Win32_Networking_WinHttp" = [ "Win32_Networking" ];
+          "Win32_Networking_WinInet" = [ "Win32_Networking" ];
+          "Win32_Networking_WinSock" = [ "Win32_Networking" ];
+          "Win32_Networking_WindowsWebServices" = [ "Win32_Networking" ];
+          "Win32_Security" = [ "Win32" ];
+          "Win32_Security_AppLocker" = [ "Win32_Security" ];
+          "Win32_Security_Authentication" = [ "Win32_Security" ];
+          "Win32_Security_Authentication_Identity" = [ "Win32_Security_Authentication" ];
+          "Win32_Security_Authentication_Identity_Provider" = [ "Win32_Security_Authentication_Identity" ];
+          "Win32_Security_Authorization" = [ "Win32_Security" ];
+          "Win32_Security_Authorization_UI" = [ "Win32_Security_Authorization" ];
+          "Win32_Security_ConfigurationSnapin" = [ "Win32_Security" ];
+          "Win32_Security_Credentials" = [ "Win32_Security" ];
+          "Win32_Security_Cryptography" = [ "Win32_Security" ];
+          "Win32_Security_Cryptography_Catalog" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_Cryptography_Certificates" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_Cryptography_Sip" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_Cryptography_UI" = [ "Win32_Security_Cryptography" ];
+          "Win32_Security_DiagnosticDataQuery" = [ "Win32_Security" ];
+          "Win32_Security_DirectoryServices" = [ "Win32_Security" ];
+          "Win32_Security_EnterpriseData" = [ "Win32_Security" ];
+          "Win32_Security_ExtensibleAuthenticationProtocol" = [ "Win32_Security" ];
+          "Win32_Security_Isolation" = [ "Win32_Security" ];
+          "Win32_Security_LicenseProtection" = [ "Win32_Security" ];
+          "Win32_Security_NetworkAccessProtection" = [ "Win32_Security" ];
+          "Win32_Security_Tpm" = [ "Win32_Security" ];
+          "Win32_Security_WinTrust" = [ "Win32_Security" ];
+          "Win32_Security_WinWlx" = [ "Win32_Security" ];
+          "Win32_Storage" = [ "Win32" ];
+          "Win32_Storage_Cabinets" = [ "Win32_Storage" ];
+          "Win32_Storage_CloudFilters" = [ "Win32_Storage" ];
+          "Win32_Storage_Compression" = [ "Win32_Storage" ];
+          "Win32_Storage_DataDeduplication" = [ "Win32_Storage" ];
+          "Win32_Storage_DistributedFileSystem" = [ "Win32_Storage" ];
+          "Win32_Storage_EnhancedStorage" = [ "Win32_Storage" ];
+          "Win32_Storage_FileHistory" = [ "Win32_Storage" ];
+          "Win32_Storage_FileServerResourceManager" = [ "Win32_Storage" ];
+          "Win32_Storage_FileSystem" = [ "Win32_Storage" ];
+          "Win32_Storage_Imapi" = [ "Win32_Storage" ];
+          "Win32_Storage_IndexServer" = [ "Win32_Storage" ];
+          "Win32_Storage_InstallableFileSystems" = [ "Win32_Storage" ];
+          "Win32_Storage_IscsiDisc" = [ "Win32_Storage" ];
+          "Win32_Storage_Jet" = [ "Win32_Storage" ];
+          "Win32_Storage_OfflineFiles" = [ "Win32_Storage" ];
+          "Win32_Storage_OperationRecorder" = [ "Win32_Storage" ];
+          "Win32_Storage_Packaging" = [ "Win32_Storage" ];
+          "Win32_Storage_Packaging_Appx" = [ "Win32_Storage_Packaging" ];
+          "Win32_Storage_Packaging_Opc" = [ "Win32_Storage_Packaging" ];
+          "Win32_Storage_ProjectedFileSystem" = [ "Win32_Storage" ];
+          "Win32_Storage_StructuredStorage" = [ "Win32_Storage" ];
+          "Win32_Storage_Vhd" = [ "Win32_Storage" ];
+          "Win32_Storage_VirtualDiskService" = [ "Win32_Storage" ];
+          "Win32_Storage_Vss" = [ "Win32_Storage" ];
+          "Win32_Storage_Xps" = [ "Win32_Storage" ];
+          "Win32_Storage_Xps_Printing" = [ "Win32_Storage_Xps" ];
+          "Win32_System" = [ "Win32" ];
+          "Win32_System_AddressBook" = [ "Win32_System" ];
+          "Win32_System_Antimalware" = [ "Win32_System" ];
+          "Win32_System_ApplicationInstallationAndServicing" = [ "Win32_System" ];
+          "Win32_System_ApplicationVerifier" = [ "Win32_System" ];
+          "Win32_System_AssessmentTool" = [ "Win32_System" ];
+          "Win32_System_ClrHosting" = [ "Win32_System" ];
+          "Win32_System_Com" = [ "Win32_System" ];
+          "Win32_System_Com_CallObj" = [ "Win32_System_Com" ];
+          "Win32_System_Com_ChannelCredentials" = [ "Win32_System_Com" ];
+          "Win32_System_Com_Events" = [ "Win32_System_Com" ];
+          "Win32_System_Com_Marshal" = [ "Win32_System_Com" ];
+          "Win32_System_Com_StructuredStorage" = [ "Win32_System_Com" ];
+          "Win32_System_Com_UI" = [ "Win32_System_Com" ];
+          "Win32_System_Com_Urlmon" = [ "Win32_System_Com" ];
+          "Win32_System_ComponentServices" = [ "Win32_System" ];
+          "Win32_System_Console" = [ "Win32_System" ];
+          "Win32_System_Contacts" = [ "Win32_System" ];
+          "Win32_System_CorrelationVector" = [ "Win32_System" ];
+          "Win32_System_DataExchange" = [ "Win32_System" ];
+          "Win32_System_DeploymentServices" = [ "Win32_System" ];
+          "Win32_System_DesktopSharing" = [ "Win32_System" ];
+          "Win32_System_DeveloperLicensing" = [ "Win32_System" ];
+          "Win32_System_Diagnostics" = [ "Win32_System" ];
+          "Win32_System_Diagnostics_Ceip" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_ClrProfiling" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_Debug" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_Debug_ActiveScript" = [ "Win32_System_Diagnostics_Debug" ];
+          "Win32_System_Diagnostics_Debug_Extensions" = [ "Win32_System_Diagnostics_Debug" ];
+          "Win32_System_Diagnostics_Etw" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_ProcessSnapshotting" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_Diagnostics_ToolHelp" = [ "Win32_System_Diagnostics" ];
+          "Win32_System_DistributedTransactionCoordinator" = [ "Win32_System" ];
+          "Win32_System_Environment" = [ "Win32_System" ];
+          "Win32_System_ErrorReporting" = [ "Win32_System" ];
+          "Win32_System_EventCollector" = [ "Win32_System" ];
+          "Win32_System_EventLog" = [ "Win32_System" ];
+          "Win32_System_EventNotificationService" = [ "Win32_System" ];
+          "Win32_System_GroupPolicy" = [ "Win32_System" ];
+          "Win32_System_HostCompute" = [ "Win32_System" ];
+          "Win32_System_HostComputeNetwork" = [ "Win32_System" ];
+          "Win32_System_HostComputeSystem" = [ "Win32_System" ];
+          "Win32_System_Hypervisor" = [ "Win32_System" ];
+          "Win32_System_IO" = [ "Win32_System" ];
+          "Win32_System_Iis" = [ "Win32_System" ];
+          "Win32_System_Ioctl" = [ "Win32_System" ];
+          "Win32_System_JobObjects" = [ "Win32_System" ];
+          "Win32_System_Js" = [ "Win32_System" ];
+          "Win32_System_Kernel" = [ "Win32_System" ];
+          "Win32_System_LibraryLoader" = [ "Win32_System" ];
+          "Win32_System_Mailslots" = [ "Win32_System" ];
+          "Win32_System_Mapi" = [ "Win32_System" ];
+          "Win32_System_Memory" = [ "Win32_System" ];
+          "Win32_System_Memory_NonVolatile" = [ "Win32_System_Memory" ];
+          "Win32_System_MessageQueuing" = [ "Win32_System" ];
+          "Win32_System_MixedReality" = [ "Win32_System" ];
+          "Win32_System_Mmc" = [ "Win32_System" ];
+          "Win32_System_Ole" = [ "Win32_System" ];
+          "Win32_System_ParentalControls" = [ "Win32_System" ];
+          "Win32_System_PasswordManagement" = [ "Win32_System" ];
+          "Win32_System_Performance" = [ "Win32_System" ];
+          "Win32_System_Performance_HardwareCounterProfiling" = [ "Win32_System_Performance" ];
+          "Win32_System_Pipes" = [ "Win32_System" ];
+          "Win32_System_Power" = [ "Win32_System" ];
+          "Win32_System_ProcessStatus" = [ "Win32_System" ];
+          "Win32_System_RealTimeCommunications" = [ "Win32_System" ];
+          "Win32_System_Recovery" = [ "Win32_System" ];
+          "Win32_System_Registry" = [ "Win32_System" ];
+          "Win32_System_RemoteAssistance" = [ "Win32_System" ];
+          "Win32_System_RemoteDesktop" = [ "Win32_System" ];
+          "Win32_System_RemoteManagement" = [ "Win32_System" ];
+          "Win32_System_RestartManager" = [ "Win32_System" ];
+          "Win32_System_Restore" = [ "Win32_System" ];
+          "Win32_System_Rpc" = [ "Win32_System" ];
+          "Win32_System_Search" = [ "Win32_System" ];
+          "Win32_System_Search_Common" = [ "Win32_System_Search" ];
+          "Win32_System_SecurityCenter" = [ "Win32_System" ];
+          "Win32_System_ServerBackup" = [ "Win32_System" ];
+          "Win32_System_Services" = [ "Win32_System" ];
+          "Win32_System_SettingsManagementInfrastructure" = [ "Win32_System" ];
+          "Win32_System_SetupAndMigration" = [ "Win32_System" ];
+          "Win32_System_Shutdown" = [ "Win32_System" ];
+          "Win32_System_StationsAndDesktops" = [ "Win32_System" ];
+          "Win32_System_SubsystemForLinux" = [ "Win32_System" ];
+          "Win32_System_SystemInformation" = [ "Win32_System" ];
+          "Win32_System_SystemServices" = [ "Win32_System" ];
+          "Win32_System_TaskScheduler" = [ "Win32_System" ];
+          "Win32_System_Threading" = [ "Win32_System" ];
+          "Win32_System_Time" = [ "Win32_System" ];
+          "Win32_System_TpmBaseServices" = [ "Win32_System" ];
+          "Win32_System_UpdateAgent" = [ "Win32_System" ];
+          "Win32_System_UpdateAssessment" = [ "Win32_System" ];
+          "Win32_System_UserAccessLogging" = [ "Win32_System" ];
+          "Win32_System_VirtualDosMachines" = [ "Win32_System" ];
+          "Win32_System_WindowsProgramming" = [ "Win32_System" ];
+          "Win32_System_WindowsSync" = [ "Win32_System" ];
+          "Win32_System_Wmi" = [ "Win32_System" ];
+          "Win32_UI" = [ "Win32" ];
+          "Win32_UI_Accessibility" = [ "Win32_UI" ];
+          "Win32_UI_Animation" = [ "Win32_UI" ];
+          "Win32_UI_ColorSystem" = [ "Win32_UI" ];
+          "Win32_UI_Controls" = [ "Win32_UI" ];
+          "Win32_UI_Controls_Dialogs" = [ "Win32_UI_Controls" ];
+          "Win32_UI_Controls_RichEdit" = [ "Win32_UI_Controls" ];
+          "Win32_UI_HiDpi" = [ "Win32_UI" ];
+          "Win32_UI_Input" = [ "Win32_UI" ];
+          "Win32_UI_Input_Ime" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Ink" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_KeyboardAndMouse" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Pointer" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Radial" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_Touch" = [ "Win32_UI_Input" ];
+          "Win32_UI_Input_XboxController" = [ "Win32_UI_Input" ];
+          "Win32_UI_InteractionContext" = [ "Win32_UI" ];
+          "Win32_UI_LegacyWindowsEnvironmentFeatures" = [ "Win32_UI" ];
+          "Win32_UI_Magnification" = [ "Win32_UI" ];
+          "Win32_UI_Notifications" = [ "Win32_UI" ];
+          "Win32_UI_Ribbon" = [ "Win32_UI" ];
+          "Win32_UI_Shell" = [ "Win32_UI" ];
+          "Win32_UI_Shell_Common" = [ "Win32_UI_Shell" ];
+          "Win32_UI_Shell_PropertiesSystem" = [ "Win32_UI_Shell" ];
+          "Win32_UI_TabletPC" = [ "Win32_UI" ];
+          "Win32_UI_TextServices" = [ "Win32_UI" ];
+          "Win32_UI_WindowsAndMessaging" = [ "Win32_UI" ];
+          "Win32_UI_Wpf" = [ "Win32_UI" ];
+          "Win32_Web" = [ "Win32" ];
+          "Win32_Web_InternetExplorer" = [ "Win32_Web" ];
+        };
+        resolvedDefaultFeatures = [ "Win32" "Win32_Foundation" "Win32_NetworkManagement" "Win32_NetworkManagement_IpHelper" "Win32_Networking" "Win32_Networking_WinSock" "Win32_Security" "Win32_Security_Authorization" "Win32_Storage" "Win32_Storage_FileSystem" "Win32_System" "Win32_System_Console" "Win32_System_Diagnostics" "Win32_System_Diagnostics_Debug" "Win32_System_IO" "Win32_System_Pipes" "Win32_System_SystemServices" "Win32_System_Threading" "default" ];
+      };
+      "windows-targets 0.42.2" = rec {
+        crateName = "windows-targets";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "0wfhnib2fisxlx8c507dbmh97kgij4r6kcxdi0f9nk6l1k080lcf";
+        authors = [
+          "Microsoft"
+        ];
+        dependencies = [
+          {
+            name = "windows_aarch64_gnullvm";
+            packageId = "windows_aarch64_gnullvm 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "aarch64-pc-windows-gnullvm");
+          }
+          {
+            name = "windows_aarch64_msvc";
+            packageId = "windows_aarch64_msvc 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "aarch64-pc-windows-msvc");
+          }
+          {
+            name = "windows_aarch64_msvc";
+            packageId = "windows_aarch64_msvc 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "aarch64-uwp-windows-msvc");
+          }
+          {
+            name = "windows_i686_gnu";
+            packageId = "windows_i686_gnu 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "i686-pc-windows-gnu");
+          }
+          {
+            name = "windows_i686_gnu";
+            packageId = "windows_i686_gnu 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "i686-uwp-windows-gnu");
+          }
+          {
+            name = "windows_i686_msvc";
+            packageId = "windows_i686_msvc 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "i686-pc-windows-msvc");
+          }
+          {
+            name = "windows_i686_msvc";
+            packageId = "windows_i686_msvc 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "i686-uwp-windows-msvc");
+          }
+          {
+            name = "windows_x86_64_gnu";
+            packageId = "windows_x86_64_gnu 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "x86_64-pc-windows-gnu");
+          }
+          {
+            name = "windows_x86_64_gnu";
+            packageId = "windows_x86_64_gnu 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "x86_64-uwp-windows-gnu");
+          }
+          {
+            name = "windows_x86_64_gnullvm";
+            packageId = "windows_x86_64_gnullvm 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "x86_64-pc-windows-gnullvm");
+          }
+          {
+            name = "windows_x86_64_msvc";
+            packageId = "windows_x86_64_msvc 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "x86_64-pc-windows-msvc");
+          }
+          {
+            name = "windows_x86_64_msvc";
+            packageId = "windows_x86_64_msvc 0.42.2";
+            target = { target, features }: (pkgs.rust.lib.toRustTarget stdenv.hostPlatform == "x86_64-uwp-windows-msvc");
+          }
+        ];
+
+      };
+      "windows-targets 0.48.0" = rec {
+        crateName = "windows-targets";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "1mfzg94w0c8h4ya9sva7rra77f3iy1712af9b6bwg03wrpqbc7kv";
+        authors = [
+          "Microsoft"
+        ];
+        dependencies = [
+          {
+            name = "windows_aarch64_gnullvm";
+            packageId = "windows_aarch64_gnullvm 0.48.0";
+            target = { target, features }: (("aarch64" == target."arch") && ("gnu" == target."env") && ("llvm" == target."abi") && (!(target."windows_raw_dylib" or false)));
+          }
+          {
+            name = "windows_aarch64_msvc";
+            packageId = "windows_aarch64_msvc 0.48.0";
+            target = { target, features }: (("aarch64" == target."arch") && ("msvc" == target."env") && (!(target."windows_raw_dylib" or false)));
+          }
+          {
+            name = "windows_i686_gnu";
+            packageId = "windows_i686_gnu 0.48.0";
+            target = { target, features }: (("x86" == target."arch") && ("gnu" == target."env") && (!(target."windows_raw_dylib" or false)));
+          }
+          {
+            name = "windows_i686_msvc";
+            packageId = "windows_i686_msvc 0.48.0";
+            target = { target, features }: (("x86" == target."arch") && ("msvc" == target."env") && (!(target."windows_raw_dylib" or false)));
+          }
+          {
+            name = "windows_x86_64_gnu";
+            packageId = "windows_x86_64_gnu 0.48.0";
+            target = { target, features }: (("x86_64" == target."arch") && ("gnu" == target."env") && (!("llvm" == target."abi")) && (!(target."windows_raw_dylib" or false)));
+          }
+          {
+            name = "windows_x86_64_gnullvm";
+            packageId = "windows_x86_64_gnullvm 0.48.0";
+            target = { target, features }: (("x86_64" == target."arch") && ("gnu" == target."env") && ("llvm" == target."abi") && (!(target."windows_raw_dylib" or false)));
+          }
+          {
+            name = "windows_x86_64_msvc";
+            packageId = "windows_x86_64_msvc 0.48.0";
+            target = { target, features }: (("x86_64" == target."arch") && ("msvc" == target."env") && (!(target."windows_raw_dylib" or false)));
+          }
+        ];
+
+      };
+      "windows_aarch64_gnullvm 0.42.2" = rec {
+        crateName = "windows_aarch64_gnullvm";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "1y4q0qmvl0lvp7syxvfykafvmwal5hrjb4fmv04bqs0bawc52yjr";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_aarch64_gnullvm 0.48.0" = rec {
+        crateName = "windows_aarch64_gnullvm";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "1g71yxi61c410pwzq05ld7si4p9hyx6lf5fkw21sinvr3cp5gbli";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_aarch64_msvc 0.42.2" = rec {
+        crateName = "windows_aarch64_msvc";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "0hsdikjl5sa1fva5qskpwlxzpc5q9l909fpl1w6yy1hglrj8i3p0";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_aarch64_msvc 0.48.0" = rec {
+        crateName = "windows_aarch64_msvc";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "1wvwipchhywcjaw73h998vzachf668fpqccbhrxzrz5xszh2gvxj";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_i686_gnu 0.42.2" = rec {
+        crateName = "windows_i686_gnu";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "0kx866dfrby88lqs9v1vgmrkk1z6af9lhaghh5maj7d4imyr47f6";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_i686_gnu 0.48.0" = rec {
+        crateName = "windows_i686_gnu";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "0hd2v9kp8fss0rzl83wzhw0s5z8q1b4875m6s1phv0yvlxi1jak2";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_i686_msvc 0.42.2" = rec {
+        crateName = "windows_i686_msvc";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "0q0h9m2aq1pygc199pa5jgc952qhcnf0zn688454i7v4xjv41n24";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_i686_msvc 0.48.0" = rec {
+        crateName = "windows_i686_msvc";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "004fkyqv3if178xx9ksqc4qqv8sz8n72mpczsr2vy8ffckiwchj5";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_x86_64_gnu 0.42.2" = rec {
+        crateName = "windows_x86_64_gnu";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "0dnbf2xnp3xrvy8v9mgs3var4zq9v9yh9kv79035rdgyp2w15scd";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_x86_64_gnu 0.48.0" = rec {
+        crateName = "windows_x86_64_gnu";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "1cblz5m6a8q6ha09bz4lz233dnq5sw2hpra06k9cna3n3xk8laya";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_x86_64_gnullvm 0.42.2" = rec {
+        crateName = "windows_x86_64_gnullvm";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "18wl9r8qbsl475j39zvawlidp1bsbinliwfymr43fibdld31pm16";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_x86_64_gnullvm 0.48.0" = rec {
+        crateName = "windows_x86_64_gnullvm";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "0lxryz3ysx0145bf3i38jkr7f9nxiym8p3syklp8f20yyk0xp5kq";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_x86_64_msvc 0.42.2" = rec {
+        crateName = "windows_x86_64_msvc";
+        version = "0.42.2";
+        edition = "2018";
+        sha256 = "1w5r0q0yzx827d10dpjza2ww0j8iajqhmb54s735hhaj66imvv4s";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "windows_x86_64_msvc 0.48.0" = rec {
+        crateName = "windows_x86_64_msvc";
+        version = "0.48.0";
+        edition = "2018";
+        sha256 = "12ipr1knzj2rwjygyllfi5mkd0ihnbi3r61gag5n2jgyk5bmyl8s";
+        authors = [
+          "Microsoft"
+        ];
+
+      };
+      "wu-manber" = rec {
+        crateName = "wu-manber";
+        version = "0.1.0";
+        edition = "2015";
+        workspace_member = null;
+        src = pkgs.fetchgit {
+          url = "https://github.com/tvlfyi/wu-manber.git";
+          rev = "e77628cafcf45d41b9e455be86a6b1b9f46b2092";
+          sha256 = "02byhfiw41mlgr1c43n2iq6jw5sbyn8l1acv5v71a07h5l18q0cy";
+        };
+        authors = [
+          "Joe Neeman <joeneeman@gmail.com>"
+        ];
+
+      };
+      "xml-rs" = rec {
+        crateName = "xml-rs";
+        version = "0.8.7";
+        edition = "2021";
+        crateBin = [ ];
+        sha256 = "18nxpxqhqbhxncii03gf56hvhyf4s3icdmlks3v7lznxph2037b9";
+        libName = "xml";
+        authors = [
+          "Vladimir Matveev <vmatveev@citrine.cc>"
+        ];
+
+      };
+      "yansi" = rec {
+        crateName = "yansi";
+        version = "0.5.1";
+        edition = "2015";
+        sha256 = "1v4qljgzh73knr7291cgwrf56zrvhmpn837n5n5pypzq1kciq109";
+        authors = [
+          "Sergio Benitez <sb@sergio.bz>"
+        ];
+
+      };
+      "zstd" = rec {
+        crateName = "zstd";
+        version = "0.9.2+zstd.1.5.1";
+        edition = "2018";
+        sha256 = "0m5aik2jy2w1g68i4isa0c3gq9a7avq9abgjfjbc6f60yqdym413";
+        authors = [
+          "Alexandre Bury <alexandre.bury@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "zstd-safe";
+            packageId = "zstd-safe";
+            usesDefaultFeatures = false;
+            features = [ "std" ];
+          }
+        ];
+        features = {
+          "arrays" = [ "zstd-safe/arrays" ];
+          "bindgen" = [ "zstd-safe/bindgen" ];
+          "debug" = [ "zstd-safe/debug" ];
+          "default" = [ "legacy" "arrays" ];
+          "experimental" = [ "zstd-safe/experimental" ];
+          "legacy" = [ "zstd-safe/legacy" ];
+          "no_asm" = [ "zstd-safe/no_asm" ];
+          "pkg-config" = [ "zstd-safe/pkg-config" ];
+          "thin" = [ "zstd-safe/thin" ];
+          "zstdmt" = [ "zstd-safe/zstdmt" ];
+        };
+        resolvedDefaultFeatures = [ "arrays" "default" "legacy" ];
+      };
+      "zstd-safe" = rec {
+        crateName = "zstd-safe";
+        version = "4.1.3+zstd.1.5.1";
+        edition = "2018";
+        sha256 = "0yfvqzzkbj871f2vaikal5rm2gf60p1mdzp3jk3w5hmkkywq37g9";
+        authors = [
+          "Alexandre Bury <alexandre.bury@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+          {
+            name = "zstd-sys";
+            packageId = "zstd-sys";
+            usesDefaultFeatures = false;
+          }
+        ];
+        features = {
+          "bindgen" = [ "zstd-sys/bindgen" ];
+          "debug" = [ "zstd-sys/debug" ];
+          "default" = [ "legacy" "arrays" ];
+          "experimental" = [ "zstd-sys/experimental" ];
+          "legacy" = [ "zstd-sys/legacy" ];
+          "no_asm" = [ "zstd-sys/no_asm" ];
+          "pkg-config" = [ "zstd-sys/pkg-config" ];
+          "std" = [ "zstd-sys/std" ];
+          "thin" = [ "zstd-sys/thin" ];
+          "zstdmt" = [ "zstd-sys/zstdmt" ];
+        };
+        resolvedDefaultFeatures = [ "arrays" "legacy" "std" ];
+      };
+      "zstd-sys" = rec {
+        crateName = "zstd-sys";
+        version = "1.6.2+zstd.1.5.1";
+        edition = "2018";
+        sha256 = "17xcr0mw8ps9hlc8m0dzj7yd52lb9r9ic9fbpxa4994yilj2zbrd";
+        authors = [
+          "Alexandre Bury <alexandre.bury@gmail.com>"
+        ];
+        dependencies = [
+          {
+            name = "libc";
+            packageId = "libc";
+          }
+        ];
+        buildDependencies = [
+          {
+            name = "cc";
+            packageId = "cc";
+            features = [ "parallel" ];
+          }
+        ];
+        features = {
+          "bindgen" = [ "dep:bindgen" ];
+          "default" = [ "legacy" ];
+          "pkg-config" = [ "dep:pkg-config" ];
+        };
+        resolvedDefaultFeatures = [ "legacy" "std" ];
+      };
+    };
+
+    #
+    # crate2nix/default.nix (excerpt start)
+    #
+
+    /* Target (platform) data for conditional dependencies.
+      This corresponds roughly to what buildRustCrate is setting.
+    */
+    makeDefaultTarget = platform: {
+      unix = platform.isUnix;
+      windows = platform.isWindows;
+      fuchsia = true;
+      test = false;
+
+      /* We are choosing an arbitrary rust version to grab `lib` from,
+      which is unfortunate, but `lib` has been version-agnostic the
+      whole time so this is good enough for now.
+      */
+      os = pkgs.rust.lib.toTargetOs platform;
+      arch = pkgs.rust.lib.toTargetArch platform;
+      family = pkgs.rust.lib.toTargetFamily platform;
+      env = "gnu";
+      endian =
+        if platform.parsed.cpu.significantByte.name == "littleEndian"
+        then "little" else "big";
+      pointer_width = toString platform.parsed.cpu.bits;
+      vendor = platform.parsed.vendor.name;
+      debug_assertions = false;
+    };
+
+    /* Filters common temp files and build files. */
+    # TODO(pkolloch): Substitute with gitignore filter
+    sourceFilter = name: type:
+      let
+        baseName = builtins.baseNameOf (builtins.toString name);
+      in
+        ! (
+          # Filter out git
+          baseName == ".gitignore"
+          || (type == "directory" && baseName == ".git")
+
+          # Filter out build results
+          || (
+            type == "directory" && (
+              baseName == "target"
+              || baseName == "_site"
+              || baseName == ".sass-cache"
+              || baseName == ".jekyll-metadata"
+              || baseName == "build-artifacts"
+            )
+          )
+
+          # Filter out nix-build result symlinks
+          || (
+            type == "symlink" && lib.hasPrefix "result" baseName
+          )
+
+          # Filter out IDE config
+          || (
+            type == "directory" && (
+              baseName == ".idea" || baseName == ".vscode"
+            )
+          ) || lib.hasSuffix ".iml" baseName
+
+          # Filter out nix build files
+          || baseName == "Cargo.nix"
+
+          # Filter out editor backup / swap files.
+          || lib.hasSuffix "~" baseName
+          || builtins.match "^\\.sw[a-z]$$" baseName != null
+          || builtins.match "^\\..*\\.sw[a-z]$$" baseName != null
+          || lib.hasSuffix ".tmp" baseName
+          || lib.hasSuffix ".bak" baseName
+          || baseName == "tests.nix"
+        );
+
+    /* Returns a crate which depends on successful test execution
+      of crate given as the second argument.
+
+      testCrateFlags: list of flags to pass to the test exectuable
+      testInputs: list of packages that should be available during test execution
+    */
+    crateWithTest = { crate, testCrate, testCrateFlags, testInputs, testPreRun, testPostRun }:
+      assert builtins.typeOf testCrateFlags == "list";
+      assert builtins.typeOf testInputs == "list";
+      assert builtins.typeOf testPreRun == "string";
+      assert builtins.typeOf testPostRun == "string";
+      let
+        # override the `crate` so that it will build and execute tests instead of
+        # building the actual lib and bin targets We just have to pass `--test`
+        # to rustc and it will do the right thing.  We execute the tests and copy
+        # their log and the test executables to $out for later inspection.
+        test =
+          let
+            drv = testCrate.override
+              (
+                _: {
+                  buildTests = true;
+                }
+              );
+            # If the user hasn't set any pre/post commands, we don't want to
+            # insert empty lines. This means that any existing users of crate2nix
+            # don't get a spurious rebuild unless they set these explicitly.
+            testCommand = pkgs.lib.concatStringsSep "\n"
+              (pkgs.lib.filter (s: s != "") [
+                testPreRun
+                "$f $testCrateFlags 2>&1 | tee -a $out"
+                testPostRun
+              ]);
+          in
+          pkgs.runCommand "run-tests-${testCrate.name}"
+            {
+              inherit testCrateFlags;
+              buildInputs = testInputs;
+            } ''
+            set -ex
+
+            export RUST_BACKTRACE=1
+
+            # recreate a file hierarchy as when running tests with cargo
+
+            # the source for test data
+            ${pkgs.xorg.lndir}/bin/lndir ${crate.src}
+
+            # build outputs
+            testRoot=target/debug
+            mkdir -p $testRoot
+
+            # executables of the crate
+            # we copy to prevent std::env::current_exe() to resolve to a store location
+            for i in ${crate}/bin/*; do
+              cp "$i" "$testRoot"
+            done
+            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)
+            for file in ${drv}/tests/*; do
+              f=$testRoot/$(basename $file)-$hash
+              cp $file $f
+              ${testCommand}
+            done
+          '';
+      in
+      pkgs.runCommand "${crate.name}-linked"
+        {
+          inherit (crate) outputs crateName;
+          passthru = (crate.passthru or { }) // {
+            inherit test;
+          };
+        } ''
+        echo tested by ${test}
+        ${lib.concatMapStringsSep "\n" (output: "ln -s ${crate.${output}} ${"$"}${output}") crate.outputs}
+      '';
+
+    /* A restricted overridable version of builtRustCratesWithFeatures. */
+    buildRustCrateWithFeatures =
+      { packageId
+      , features ? rootFeatures
+      , crateOverrides ? defaultCrateOverrides
+      , buildRustCrateForPkgsFunc ? null
+      , runTests ? false
+      , testCrateFlags ? [ ]
+      , testInputs ? [ ]
+        # Any command to run immediatelly before a test is executed.
+      , testPreRun ? ""
+        # Any command run immediatelly after a test is executed.
+      , testPostRun ? ""
+      }:
+      lib.makeOverridable
+        (
+          { features
+          , crateOverrides
+          , runTests
+          , testCrateFlags
+          , testInputs
+          , testPreRun
+          , testPostRun
+          }:
+          let
+            buildRustCrateForPkgsFuncOverriden =
+              if buildRustCrateForPkgsFunc != null
+              then buildRustCrateForPkgsFunc
+              else
+                (
+                  if crateOverrides == pkgs.defaultCrateOverrides
+                  then buildRustCrateForPkgs
+                  else
+                    pkgs: (buildRustCrateForPkgs pkgs).override {
+                      defaultCrateOverrides = crateOverrides;
+                    }
+                );
+            builtRustCrates = builtRustCratesWithFeatures {
+              inherit packageId features;
+              buildRustCrateForPkgsFunc = buildRustCrateForPkgsFuncOverriden;
+              runTests = false;
+            };
+            builtTestRustCrates = builtRustCratesWithFeatures {
+              inherit packageId features;
+              buildRustCrateForPkgsFunc = buildRustCrateForPkgsFuncOverriden;
+              runTests = true;
+            };
+            drv = builtRustCrates.crates.${packageId};
+            testDrv = builtTestRustCrates.crates.${packageId};
+            derivation =
+              if runTests then
+                crateWithTest
+                  {
+                    crate = drv;
+                    testCrate = testDrv;
+                    inherit testCrateFlags testInputs testPreRun testPostRun;
+                  }
+              else drv;
+          in
+          derivation
+        )
+        { inherit features crateOverrides runTests testCrateFlags testInputs testPreRun testPostRun; };
+
+    /* Returns an attr set with packageId mapped to the result of buildRustCrateForPkgsFunc
+      for the corresponding crate.
+    */
+    builtRustCratesWithFeatures =
+      { packageId
+      , features
+      , crateConfigs ? crates
+      , buildRustCrateForPkgsFunc
+      , runTests
+      , makeTarget ? makeDefaultTarget
+      } @ args:
+        assert (builtins.isAttrs crateConfigs);
+        assert (builtins.isString packageId);
+        assert (builtins.isList features);
+        assert (builtins.isAttrs (makeTarget stdenv.hostPlatform));
+        assert (builtins.isBool runTests);
+        let
+          rootPackageId = packageId;
+          mergedFeatures = mergePackageFeatures
+            (
+              args // {
+                inherit rootPackageId;
+                target = makeTarget stdenv.hostPlatform // { test = runTests; };
+              }
+            );
+          # Memoize built packages so that reappearing packages are only built once.
+          builtByPackageIdByPkgs = mkBuiltByPackageIdByPkgs pkgs;
+          mkBuiltByPackageIdByPkgs = pkgs:
+            let
+              self = {
+                crates = lib.mapAttrs (packageId: value: buildByPackageIdForPkgsImpl self pkgs packageId) crateConfigs;
+                target = makeTarget pkgs.stdenv.hostPlatform;
+                build = mkBuiltByPackageIdByPkgs pkgs.buildPackages;
+              };
+            in
+            self;
+          buildByPackageIdForPkgsImpl = self: pkgs: packageId:
+            let
+              features = mergedFeatures."${packageId}" or [ ];
+              crateConfig' = crateConfigs."${packageId}";
+              crateConfig =
+                builtins.removeAttrs crateConfig' [ "resolvedDefaultFeatures" "devDependencies" ];
+              devDependencies =
+                lib.optionals
+                  (runTests && packageId == rootPackageId)
+                  (crateConfig'.devDependencies or [ ]);
+              dependencies =
+                dependencyDerivations {
+                  inherit features;
+                  inherit (self) target;
+                  buildByPackageId = depPackageId:
+                    # proc_macro crates must be compiled for the build architecture
+                    if crateConfigs.${depPackageId}.procMacro or false
+                    then self.build.crates.${depPackageId}
+                    else self.crates.${depPackageId};
+                  dependencies =
+                    (crateConfig.dependencies or [ ])
+                    ++ devDependencies;
+                };
+              buildDependencies =
+                dependencyDerivations {
+                  inherit features;
+                  inherit (self.build) target;
+                  buildByPackageId = depPackageId:
+                    self.build.crates.${depPackageId};
+                  dependencies = crateConfig.buildDependencies or [ ];
+                };
+              dependenciesWithRenames =
+                let
+                  buildDeps = filterEnabledDependencies {
+                    inherit features;
+                    inherit (self) target;
+                    dependencies = crateConfig.dependencies or [ ] ++ devDependencies;
+                  };
+                  hostDeps = filterEnabledDependencies {
+                    inherit features;
+                    inherit (self.build) target;
+                    dependencies = crateConfig.buildDependencies or [ ];
+                  };
+                in
+                lib.filter (d: d ? "rename") (hostDeps ++ buildDeps);
+              # Crate renames have the form:
+              #
+              # {
+              #    crate_name = [
+              #       { version = "1.2.3"; rename = "crate_name01"; }
+              #    ];
+              #    # ...
+              # }
+              crateRenames =
+                let
+                  grouped =
+                    lib.groupBy
+                      (dependency: dependency.name)
+                      dependenciesWithRenames;
+                  versionAndRename = dep:
+                    let
+                      package = crateConfigs."${dep.packageId}";
+                    in
+                    { inherit (dep) rename; version = package.version; };
+                in
+                lib.mapAttrs (name: choices: builtins.map versionAndRename choices) grouped;
+            in
+            buildRustCrateForPkgsFunc pkgs
+              (
+                crateConfig // {
+                  src = crateConfig.src or (
+                    pkgs.fetchurl rec {
+                      name = "${crateConfig.crateName}-${crateConfig.version}.tar.gz";
+                      # https://www.pietroalbini.org/blog/downloading-crates-io/
+                      # Not rate-limited, CDN URL.
+                      url = "https://static.crates.io/crates/${crateConfig.crateName}/${crateConfig.crateName}-${crateConfig.version}.crate";
+                      sha256 =
+                        assert (lib.assertMsg (crateConfig ? sha256) "Missing sha256 for ${name}");
+                        crateConfig.sha256;
+                    }
+                  );
+                  extraRustcOpts = lib.lists.optional (targetFeatures != [ ]) "-C target-feature=${lib.concatMapStringsSep "," (x: "+${x}") targetFeatures}";
+                  inherit features dependencies buildDependencies crateRenames release;
+                }
+              );
+        in
+        builtByPackageIdByPkgs;
+
+    /* Returns the actual derivations for the given dependencies. */
+    dependencyDerivations =
+      { buildByPackageId
+      , features
+      , dependencies
+      , target
+      }:
+        assert (builtins.isList features);
+        assert (builtins.isList dependencies);
+        assert (builtins.isAttrs target);
+        let
+          enabledDependencies = filterEnabledDependencies {
+            inherit dependencies features target;
+          };
+          depDerivation = dependency: buildByPackageId dependency.packageId;
+        in
+        map depDerivation enabledDependencies;
+
+    /* Returns a sanitized version of val with all values substituted that cannot
+      be serialized as JSON.
+    */
+    sanitizeForJson = val:
+      if builtins.isAttrs val
+      then lib.mapAttrs (n: v: sanitizeForJson v) val
+      else if builtins.isList val
+      then builtins.map sanitizeForJson val
+      else if builtins.isFunction val
+      then "function"
+      else val;
+
+    /* Returns various tools to debug a crate. */
+    debugCrate = { packageId, target ? makeDefaultTarget stdenv.hostPlatform }:
+      assert (builtins.isString packageId);
+      let
+        debug = rec {
+          # The built tree as passed to buildRustCrate.
+          buildTree = buildRustCrateWithFeatures {
+            buildRustCrateForPkgsFunc = _: lib.id;
+            inherit packageId;
+          };
+          sanitizedBuildTree = sanitizeForJson buildTree;
+          dependencyTree = sanitizeForJson
+            (
+              buildRustCrateWithFeatures {
+                buildRustCrateForPkgsFunc = _: crate: {
+                  "01_crateName" = crate.crateName or false;
+                  "02_features" = crate.features or [ ];
+                  "03_dependencies" = crate.dependencies or [ ];
+                };
+                inherit packageId;
+              }
+            );
+          mergedPackageFeatures = mergePackageFeatures {
+            features = rootFeatures;
+            inherit packageId target;
+          };
+          diffedDefaultPackageFeatures = diffDefaultPackageFeatures {
+            inherit packageId target;
+          };
+        };
+      in
+      { internal = debug; };
+
+    /* Returns differences between cargo default features and crate2nix default
+      features.
+
+      This is useful for verifying the feature resolution in crate2nix.
+    */
+    diffDefaultPackageFeatures =
+      { crateConfigs ? crates
+      , packageId
+      , target
+      }:
+        assert (builtins.isAttrs crateConfigs);
+        let
+          prefixValues = prefix: lib.mapAttrs (n: v: { "${prefix}" = v; });
+          mergedFeatures =
+            prefixValues
+              "crate2nix"
+              (mergePackageFeatures { inherit crateConfigs packageId target; features = [ "default" ]; });
+          configs = prefixValues "cargo" crateConfigs;
+          combined = lib.foldAttrs (a: b: a // b) { } [ mergedFeatures configs ];
+          onlyInCargo =
+            builtins.attrNames
+              (lib.filterAttrs (n: v: !(v ? "crate2nix") && (v ? "cargo")) combined);
+          onlyInCrate2Nix =
+            builtins.attrNames
+              (lib.filterAttrs (n: v: (v ? "crate2nix") && !(v ? "cargo")) combined);
+          differentFeatures = lib.filterAttrs
+            (
+              n: v:
+                (v ? "crate2nix")
+                && (v ? "cargo")
+                && (v.crate2nix.features or [ ]) != (v."cargo".resolved_default_features or [ ])
+            )
+            combined;
+        in
+        builtins.toJSON {
+          inherit onlyInCargo onlyInCrate2Nix differentFeatures;
+        };
+
+    /* Returns an attrset mapping packageId to the list of enabled features.
+
+      If multiple paths to a dependency enable different features, the
+      corresponding feature sets are merged. Features in rust are additive.
+    */
+    mergePackageFeatures =
+      { crateConfigs ? crates
+      , packageId
+      , rootPackageId ? packageId
+      , features ? rootFeatures
+      , dependencyPath ? [ crates.${packageId}.crateName ]
+      , featuresByPackageId ? { }
+      , target
+        # Adds devDependencies to the crate with rootPackageId.
+      , runTests ? false
+      , ...
+      } @ args:
+        assert (builtins.isAttrs crateConfigs);
+        assert (builtins.isString packageId);
+        assert (builtins.isString rootPackageId);
+        assert (builtins.isList features);
+        assert (builtins.isList dependencyPath);
+        assert (builtins.isAttrs featuresByPackageId);
+        assert (builtins.isAttrs target);
+        assert (builtins.isBool runTests);
+        let
+          crateConfig = crateConfigs."${packageId}" or (builtins.throw "Package not found: ${packageId}");
+          expandedFeatures = expandFeatures (crateConfig.features or { }) features;
+          enabledFeatures = enableFeatures (crateConfig.dependencies or [ ]) expandedFeatures;
+          depWithResolvedFeatures = dependency:
+            let
+              packageId = dependency.packageId;
+              features = dependencyFeatures enabledFeatures dependency;
+            in
+            { inherit packageId features; };
+          resolveDependencies = cache: path: dependencies:
+            assert (builtins.isAttrs cache);
+            assert (builtins.isList dependencies);
+            let
+              enabledDependencies = filterEnabledDependencies {
+                inherit dependencies target;
+                features = enabledFeatures;
+              };
+              directDependencies = map depWithResolvedFeatures enabledDependencies;
+              foldOverCache = op: lib.foldl op cache directDependencies;
+            in
+            foldOverCache
+              (
+                cache: { packageId, features }:
+                  let
+                    cacheFeatures = cache.${packageId} or [ ];
+                    combinedFeatures = sortedUnique (cacheFeatures ++ features);
+                  in
+                  if cache ? ${packageId} && cache.${packageId} == combinedFeatures
+                  then cache
+                  else
+                    mergePackageFeatures {
+                      features = combinedFeatures;
+                      featuresByPackageId = cache;
+                      inherit crateConfigs packageId target runTests rootPackageId;
+                    }
+              );
+          cacheWithSelf =
+            let
+              cacheFeatures = featuresByPackageId.${packageId} or [ ];
+              combinedFeatures = sortedUnique (cacheFeatures ++ enabledFeatures);
+            in
+            featuresByPackageId // {
+              "${packageId}" = combinedFeatures;
+            };
+          cacheWithDependencies =
+            resolveDependencies cacheWithSelf "dep"
+              (
+                crateConfig.dependencies or [ ]
+                ++ lib.optionals
+                  (runTests && packageId == rootPackageId)
+                  (crateConfig.devDependencies or [ ])
+              );
+          cacheWithAll =
+            resolveDependencies
+              cacheWithDependencies "build"
+              (crateConfig.buildDependencies or [ ]);
+        in
+        cacheWithAll;
+
+    /* Returns the enabled dependencies given the enabled features. */
+    filterEnabledDependencies = { dependencies, features, target }:
+      assert (builtins.isList dependencies);
+      assert (builtins.isList features);
+      assert (builtins.isAttrs target);
+
+      lib.filter
+        (
+          dep:
+          let
+            targetFunc = dep.target or (features: true);
+          in
+          targetFunc { inherit features target; }
+          && (
+            !(dep.optional or false)
+            || builtins.any (doesFeatureEnableDependency dep) features
+          )
+        )
+        dependencies;
+
+    /* Returns whether the given feature should enable the given dependency. */
+    doesFeatureEnableDependency = dependency: feature:
+      let
+        name = dependency.rename or dependency.name;
+        prefix = "${name}/";
+        len = builtins.stringLength prefix;
+        startsWithPrefix = builtins.substring 0 len feature == prefix;
+      in
+      feature == name || feature == "dep:" + name || startsWithPrefix;
+
+    /* Returns the expanded features for the given inputFeatures by applying the
+      rules in featureMap.
+
+      featureMap is an attribute set which maps feature names to lists of further
+      feature names to enable in case this feature is selected.
+    */
+    expandFeatures = featureMap: inputFeatures:
+      assert (builtins.isAttrs featureMap);
+      assert (builtins.isList inputFeatures);
+      let
+        expandFeature = feature:
+          assert (builtins.isString feature);
+          [ feature ] ++ (expandFeatures featureMap (featureMap."${feature}" or [ ]));
+        outFeatures = lib.concatMap expandFeature inputFeatures;
+      in
+      sortedUnique outFeatures;
+
+    /* This function adds optional dependencies as features if they are enabled
+      indirectly by dependency features. This function mimics Cargo's behavior
+      described in a note at:
+      https://doc.rust-lang.org/nightly/cargo/reference/features.html#dependency-features
+    */
+    enableFeatures = dependencies: features:
+      assert (builtins.isList features);
+      assert (builtins.isList dependencies);
+      let
+        additionalFeatures = lib.concatMap
+          (
+            dependency:
+              assert (builtins.isAttrs dependency);
+              let
+                enabled = builtins.any (doesFeatureEnableDependency dependency) features;
+              in
+              if (dependency.optional or false) && enabled
+              then [ (dependency.rename or dependency.name) ]
+              else [ ]
+          )
+          dependencies;
+      in
+      sortedUnique (features ++ additionalFeatures);
+
+    /*
+      Returns the actual features for the given dependency.
+
+      features: The features of the crate that refers this dependency.
+    */
+    dependencyFeatures = features: dependency:
+      assert (builtins.isList features);
+      assert (builtins.isAttrs dependency);
+      let
+        defaultOrNil =
+          if dependency.usesDefaultFeatures or true
+          then [ "default" ]
+          else [ ];
+        explicitFeatures = dependency.features or [ ];
+        additionalDependencyFeatures =
+          let
+            dependencyPrefix = (dependency.rename or dependency.name) + "/";
+            dependencyFeatures =
+              builtins.filter (f: lib.hasPrefix dependencyPrefix f) features;
+          in
+          builtins.map (lib.removePrefix dependencyPrefix) dependencyFeatures;
+      in
+      defaultOrNil ++ explicitFeatures ++ additionalDependencyFeatures;
+
+    /* Sorts and removes duplicates from a list of strings. */
+    sortedUnique = features:
+      assert (builtins.isList features);
+      assert (builtins.all builtins.isString features);
+      let
+        outFeaturesSet = lib.foldl (set: feature: set // { "${feature}" = 1; }) { } features;
+        outFeaturesUnique = builtins.attrNames outFeaturesSet;
+      in
+      builtins.sort (a: b: a < b) outFeaturesUnique;
+
+    deprecationWarning = message: value:
+      if strictDeprecation
+      then builtins.throw "strictDeprecation enabled, aborting: ${message}"
+      else builtins.trace message value;
+
+    #
+    # crate2nix/default.nix (excerpt end)
+    #
+  };
+}
+
diff --git a/tvix/Cargo.toml b/tvix/Cargo.toml
new file mode 100644
index 000000000000..314e329e6f2a
--- /dev/null
+++ b/tvix/Cargo.toml
@@ -0,0 +1,35 @@
+# This Cargo file is a workspace configuration as per
+# https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html
+#
+# We add this file to get a coherent set of dependencies across Tvix
+# crates by sharing a Cargo.lock. This is necessary because of the
+# currently limited support for Rust builds in Nix.
+#
+# Note that this explicitly does *not* mean that //tvix should be
+# considered "one project": This is simply a workaround for a
+# technical limitation and it should be our aim to remove this
+# workspace file and make the subprojects independent.
+#
+# Note also that CI targets for actual projects should *not* be tied
+# to //tvix, but to its subprojects. A potential file at
+# //tvix/default.nix should likely *not* expose anything other than
+# extra steps or other auxiliary targets.
+
+[workspace]
+
+members = [
+  "cli",
+  "eval",
+  "eval/builtin-macros",
+  "nix_cli",
+  "nix-compat",
+  "serde",
+  "store",
+]
+
+# Add a profile to all targets that enables release optimisations, but
+# retains debug symbols. This is great for use with
+# benchmarking/profiling tools.
+[profile.release-with-debug]
+inherits = "release"
+debug = true
diff --git a/tvix/LICENSE b/tvix/LICENSE
new file mode 100644
index 000000000000..f288702d2fa1
--- /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 000000000000..ab6d4f44ad13
--- /dev/null
+++ b/tvix/OWNERS
@@ -0,0 +1,7 @@
+set noparent
+
+adisbladis
+flokli
+grfn
+sterni
+tazjin
diff --git a/tvix/README.md b/tvix/README.md
new file mode 100644
index 000000000000..02f3b480ba4e
--- /dev/null
+++ b/tvix/README.md
@@ -0,0 +1,104 @@
+<div align="center">
+  <img src="https://tvix.dev/tvix-logo.webp">
+</div>
+
+-----------------
+
+Tvix is a new implementation of the Nix language and package manager. See the
+[announcement post][post-1] for information about the background of this
+project.
+
+Tvix is developed by [TVL][tvl] in our monorepo, the `depot`, at
+[//tvix][tvix-src]. Code reviews take place on [Gerrit][tvix-gerrit], bugs are
+filed in [our issue tracker][b].
+
+For more information about Tvix, feel free to reach out. We are interested in
+people who would like to help us review designs, brainstorm and describe
+requirements that we may not yet have considered.
+
+Most of the discussion around development happens on our IRC channel, which you
+can join in several ways documented on [tvl.fyi][getting-in-touch], or on our
+[mailing list][].
+
+Contributions to Tvix follow the TVL [review flow][review-docs] and
+[contribution guidelines][contributing].
+
+[post-1]: https://tvl.fyi/blog/rewriting-nix
+[tvl]: https://tvl.fyi
+[tvix-src]: https://cs.tvl.fyi/depot/-/tree/tvix/
+[tvix-gerrit]: https://cl.tvl.fyi/q/path:%255Etvix.*
+[b]: https://b.tvl.fyi
+[getting-in-touch]: https://tvl.fyi/#getting-in-touch
+[mailing list]: https://inbox.tvl.su
+[review-docs]: https://code.tvl.fyi/about/docs/REVIEWS.md
+[contributing]: https://code.tvl.fyi/about/docs/CONTRIBUTING.md
+
+WARNING: Tvix is not ready for use in production. None of our current APIs
+should be considered stable in any way.
+
+WARNING: Any other instances of this project or repository are
+[`josh`-mirrors][josh]. We do not accept code contributions or issues outside of
+the tooling and communication methods outlined above.
+
+[josh]: https://github.com/josh-project/josh
+
+## Components
+
+This folder contains the following components:
+
+* `//tvix/eval` - an implementation of the Nix programming language
+* `//tvix/nix-compat` - library functions for compatibility with C++ Nix
+* `//tvix/cli` - preliminary REPL & CLI implementation for Tvix
+* `//tvix/serde` - Rust library for using the Nix language for app configuration
+* `//tvix/store` - implementation of a file store for Tvix
+
+Some additional folders with auxiliary things exist and can be explored at your
+leisure.
+
+## Building the CLI
+
+The CLI can also be built with standard Rust tooling (i.e. `cargo build`),
+as long as you are in a shell with the right dependencies.
+
+ - If you cloned the full monorepo, it can be provided by
+   `mg shell //tvix:shell`.
+ - If you cloned the `tvix` workspace only
+   (`git clone https://code.tvl.fyi/depot.git:workspace=views/tvix.git`),
+   `nix-shell` provides it.
+
+If you're in the TVL monorepo, you can also run `mg build //tvix/cli`
+(or `mg build` from inside that folder) for a more incremental build.
+
+Please follow the depot-wide instructions on how to get `mg` and use the depot
+tooling.
+
+### Compatibility
+**Important note:** We only use and test Nix builds of our software
+against Nix 2.3. There are a variety of bugs and subtle problems in
+newer Nix versions which we do not have the bandwidth to address,
+builds in newer Nix versions may or may not work.
+
+## Rust projects, crate2nix
+
+Some parts of Tvix are written in Rust. To simplify the dependency
+management on the Nix side of these builds, we use `crate2nix` in a
+single Rust workspace in `//tvix` to maintain the Nix build
+configuration.
+
+When making changes to Cargo dependency configuration in any of the
+Rust projects under `//tvix`, be sure to run
+`mg run //tvix:crate2nixGenerate --` in `//tvix` itself and commit the changes
+to the generated `Cargo.nix` file. This only applies to the full TVL checkout.
+
+## 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/cli/Cargo.toml b/tvix/cli/Cargo.toml
new file mode 100644
index 000000000000..676da02a07a2
--- /dev/null
+++ b/tvix/cli/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "tvix-cli"
+version = "0.1.0"
+edition = "2021"
+
+[[bin]]
+name = "tvix"
+path = "src/main.rs"
+
+[dependencies]
+nix-compat = { path = "../nix-compat" }
+tvix-store = { path = "../store" }
+tvix-eval = { path = "../eval" }
+rustyline = "10.0.0"
+clap = { version = "4.0", features = ["derive", "env"] }
+dirs = "4.0.0"
+smol_str = "0.2.0"
+ssri = "7.0.0"
+data-encoding = "2.3.3"
+thiserror = "1.0.38"
+
+[dependencies.wu-manber]
+git = "https://github.com/tvlfyi/wu-manber.git"
diff --git a/tvix/cli/default.nix b/tvix/cli/default.nix
new file mode 100644
index 000000000000..a4bb19701f01
--- /dev/null
+++ b/tvix/cli/default.nix
@@ -0,0 +1,36 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  mkNixpkgsEvalCheck = attrset: expectedPath: {
+    label = ":nix: evaluate nixpkgs.${attrset} in tvix";
+    needsOutput = true;
+
+    command = pkgs.writeShellScript "tvix-eval-${builtins.replaceStrings [".drv"] ["-drv"] attrset}" ''
+      TVIX_OUTPUT=$(result/bin/tvix -E '(import ${pkgs.path} {}).${attrset}')
+      EXPECTED='${/* the verbatim expected Tvix output: */ "=> \"${expectedPath}\" :: string"}'
+
+      echo "Tvix output: ''${TVIX_OUTPUT}"
+      if [ "$TVIX_OUTPUT" != "$EXPECTED" ]; then
+        echo "Correct would have been ''${EXPECTED}"
+        exit 1
+      fi
+
+      echo "Output was correct."
+    '';
+  };
+in
+
+(depot.tvix.crates.workspaceMembers.tvix-cli.build.override {
+  runTests = true;
+}).overrideAttrs (_: {
+  meta = {
+    ci.extraSteps = {
+      eval-nixpkgs-stdenv-drvpath = (mkNixpkgsEvalCheck "stdenv.drvPath" pkgs.stdenv.drvPath);
+      eval-nixpkgs-stdenv-outpath = (mkNixpkgsEvalCheck "stdenv.outPath" pkgs.stdenv.outPath);
+      eval-nixpkgs-hello-outpath = (mkNixpkgsEvalCheck "hello.outPath" pkgs.hello.outPath);
+
+      # This is the furthest we get starting with stdenv we hit something similar to b/261
+      eval-nixpkgs-cross-gcc-outpath = (mkNixpkgsEvalCheck "pkgsCross.aarch64-multiplatform.buildPackages.gcc.outPath" pkgs.pkgsCross.aarch64-multiplatform.buildPackages.gcc.outPath);
+    };
+  };
+})
diff --git a/tvix/cli/src/.skip-subtree b/tvix/cli/src/.skip-subtree
new file mode 100644
index 000000000000..a16a2afe1f1e
--- /dev/null
+++ b/tvix/cli/src/.skip-subtree
@@ -0,0 +1 @@
+Because of the derivation.nix file ...
diff --git a/tvix/cli/src/derivation.nix b/tvix/cli/src/derivation.nix
new file mode 100644
index 000000000000..9355cc3a96f0
--- /dev/null
+++ b/tvix/cli/src/derivation.nix
@@ -0,0 +1,36 @@
+# LGPL-2.1-or-later
+#
+# taken from: https://github.com/NixOS/nix/blob/master/src/libexpr/primops/derivation.nix
+#
+# TODO: rewrite in native Rust code
+
+/* This is the implementation of the ‘derivation’ builtin function.
+   It's actually a wrapper around the ‘derivationStrict’ primop. */
+
+drvAttrs @ { outputs ? [ "out" ], ... }:
+
+let
+
+  strict = derivationStrict drvAttrs;
+
+  commonAttrs = drvAttrs // (builtins.listToAttrs outputsList) //
+    {
+      all = map (x: x.value) outputsList;
+      inherit drvAttrs;
+    };
+
+  outputToAttrListElement = outputName:
+    {
+      name = outputName;
+      value = commonAttrs // {
+        outPath = builtins.getAttr outputName strict;
+        drvPath = strict.drvPath;
+        type = "derivation";
+        inherit outputName;
+      };
+    };
+
+  outputsList = map outputToAttrListElement outputs;
+
+in
+(builtins.head outputsList).value
diff --git a/tvix/cli/src/derivation.rs b/tvix/cli/src/derivation.rs
new file mode 100644
index 000000000000..cf15ebbb0dc3
--- /dev/null
+++ b/tvix/cli/src/derivation.rs
@@ -0,0 +1,697 @@
+//! Implements `builtins.derivation`, the core of what makes Nix build packages.
+use nix_compat::derivation::Derivation;
+use nix_compat::nixhash;
+use std::cell::RefCell;
+use std::collections::{btree_map, BTreeSet};
+use std::rc::Rc;
+use tvix_eval::builtin_macros::builtins;
+use tvix_eval::generators::{self, GenCo};
+use tvix_eval::{AddContext, CoercionKind, ErrorKind, NixAttrs, NixList, Value};
+
+use crate::errors::Error;
+use crate::known_paths::{KnownPaths, PathKind, PathName};
+
+// Constants used for strangely named fields in derivation inputs.
+const STRUCTURED_ATTRS: &str = "__structuredAttrs";
+const IGNORE_NULLS: &str = "__ignoreNulls";
+
+/// Helper function for populating the `drv.outputs` field from a
+/// manually specified set of outputs, instead of the default
+/// `outputs`.
+async fn populate_outputs(
+    co: &GenCo,
+    drv: &mut Derivation,
+    outputs: NixList,
+) -> Result<(), ErrorKind> {
+    // Remove the original default `out` output.
+    drv.outputs.clear();
+
+    for output in outputs {
+        let output_name = generators::request_force(co, output)
+            .await
+            .to_str()
+            .context("determining output name")?;
+
+        if drv
+            .outputs
+            .insert(output_name.as_str().into(), Default::default())
+            .is_some()
+        {
+            return Err(Error::DuplicateOutput(output_name.as_str().into()).into());
+        }
+    }
+
+    Ok(())
+}
+
+/// Populate the inputs of a derivation from the build references
+/// found when scanning the derivation's parameters.
+fn populate_inputs<I: IntoIterator<Item = PathName>>(
+    drv: &mut Derivation,
+    known_paths: &KnownPaths,
+    references: I,
+) {
+    for reference in references.into_iter() {
+        let reference = &known_paths[&reference];
+        match &reference.kind {
+            PathKind::Plain => {
+                drv.input_sources.insert(reference.path.clone());
+            }
+
+            PathKind::Output { name, derivation } => {
+                match drv.input_derivations.entry(derivation.clone()) {
+                    btree_map::Entry::Vacant(entry) => {
+                        entry.insert(BTreeSet::from([name.clone()]));
+                    }
+
+                    btree_map::Entry::Occupied(mut entry) => {
+                        entry.get_mut().insert(name.clone());
+                    }
+                }
+            }
+
+            PathKind::Derivation { output_names } => {
+                match drv.input_derivations.entry(reference.path.clone()) {
+                    btree_map::Entry::Vacant(entry) => {
+                        entry.insert(output_names.clone());
+                    }
+
+                    btree_map::Entry::Occupied(mut entry) => {
+                        entry.get_mut().extend(output_names.clone().into_iter());
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// Populate the output configuration of a derivation based on the
+/// parameters passed to the call, flipping the required
+/// parameters for a fixed-output derivation if necessary.
+///
+/// This function handles all possible combinations of the
+/// parameters, including invalid ones.
+///
+/// Due to the support for SRI hashes, and how these are passed along to
+/// builtins.derivation, outputHash and outputHashAlgo can have values which
+/// need to be further modified before constructing the Derivation struct.
+///
+/// If outputHashAlgo is an SRI hash, outputHashAlgo must either be an empty
+/// string, or the hash algorithm as specified in the (single) SRI (entry).
+/// SRI strings with multiple hash algorithms are not supported.
+///
+/// In case an SRI string was used, the (single) fixed output is populated
+/// with the hash algo name, and the hash digest is populated with the
+/// (lowercase) hex encoding of the digest.
+///
+/// These values are only rewritten for the outputs, not what's passed to env.
+fn populate_output_configuration(
+    drv: &mut Derivation,
+    hash: Option<String>,      // in nix: outputHash
+    hash_algo: Option<String>, // in nix: outputHashAlgo
+    hash_mode: Option<String>, // in nix: outputHashmode
+) -> Result<(), ErrorKind> {
+    // We only do something when `digest` and `algo` are `Some(_)``, and
+    // there's an `out` output.
+    if let (Some(hash), Some(algo), hash_mode) = (hash, hash_algo, hash_mode) {
+        match drv.outputs.get_mut("out") {
+            None => return Err(Error::ConflictingOutputTypes.into()),
+            Some(out) => {
+                // treat an empty algo as None
+                let a = if algo.is_empty() {
+                    None
+                } else {
+                    Some(algo.as_ref())
+                };
+
+                let output_hash = nixhash::from_str(&hash, a).map_err(Error::InvalidOutputHash)?;
+
+                // construct the NixHashWithMode.
+                out.hash_with_mode = match hash_mode.as_deref() {
+                    None | Some("flat") => Some(nixhash::NixHashWithMode::Flat(
+                        nixhash::NixHash::new(output_hash.algo, output_hash.digest),
+                    )),
+                    Some("recursive") => Some(nixhash::NixHashWithMode::Recursive(
+                        nixhash::NixHash::new(output_hash.algo, output_hash.digest),
+                    )),
+                    Some(other) => {
+                        return Err(Error::InvalidOutputHashMode(other.to_string()).into())
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
+/// Handles derivation parameters which are not just forwarded to
+/// the environment. The return value indicates whether the
+/// parameter should be included in the environment.
+async fn handle_derivation_parameters(
+    drv: &mut Derivation,
+    co: &GenCo,
+    name: &str,
+    value: &Value,
+    val_str: &str,
+) -> Result<bool, ErrorKind> {
+    match name {
+        IGNORE_NULLS => return Ok(false),
+
+        // Command line arguments to the builder.
+        "args" => {
+            let args = value.to_list()?;
+            for arg in args {
+                drv.arguments.push(strong_coerce_to_string(co, arg).await?);
+            }
+
+            // The arguments do not appear in the environment.
+            return Ok(false);
+        }
+
+        // Explicitly specified drv outputs (instead of default [ "out" ])
+        "outputs" => {
+            let outputs = value
+                .to_list()
+                .context("looking at the `outputs` parameter of the derivation")?;
+
+            populate_outputs(co, drv, outputs).await?;
+        }
+
+        "builder" => {
+            drv.builder = val_str.to_string();
+        }
+
+        "system" => {
+            drv.system = val_str.to_string();
+        }
+
+        _ => {}
+    }
+
+    Ok(true)
+}
+
+async fn strong_coerce_to_string(co: &GenCo, val: Value) -> Result<String, ErrorKind> {
+    let val = generators::request_force(co, val).await;
+    let val_str = generators::request_string_coerce(co, val, CoercionKind::Strong).await;
+
+    Ok(val_str.as_str().to_string())
+}
+
+#[builtins(state = "Rc<RefCell<KnownPaths>>")]
+mod derivation_builtins {
+    use super::*;
+    use nix_compat::store_path::hash_placeholder;
+    use tvix_eval::generators::Gen;
+
+    #[builtin("placeholder")]
+    async fn builtin_placeholder(co: GenCo, input: Value) -> Result<Value, ErrorKind> {
+        let placeholder = hash_placeholder(
+            input
+                .to_str()
+                .context("looking at output name in builtins.placeholder")?
+                .as_str(),
+        );
+
+        Ok(placeholder.into())
+    }
+
+    /// Strictly construct a Nix derivation from the supplied arguments.
+    ///
+    /// This is considered an internal function, users usually want to
+    /// use the higher-level `builtins.derivation` instead.
+    #[builtin("derivationStrict")]
+    async fn builtin_derivation_strict(
+        state: Rc<RefCell<KnownPaths>>,
+        co: GenCo,
+        input: Value,
+    ) -> Result<Value, ErrorKind> {
+        let input = input.to_attrs()?;
+        let name = generators::request_force(&co, input.select_required("name")?.clone())
+            .await
+            .to_str()
+            .context("determining derivation name")?;
+
+        // Check whether attributes should be passed as a JSON file.
+        // TODO: the JSON serialisation has to happen here.
+        if let Some(sa) = input.select(STRUCTURED_ATTRS) {
+            if generators::request_force(&co, sa.clone()).await.as_bool()? {
+                return Err(ErrorKind::NotImplemented(STRUCTURED_ATTRS));
+            }
+        }
+
+        // Check whether null attributes should be ignored or passed through.
+        let ignore_nulls = match input.select(IGNORE_NULLS) {
+            Some(b) => generators::request_force(&co, b.clone()).await.as_bool()?,
+            None => false,
+        };
+
+        let mut drv = Derivation::default();
+        drv.outputs.insert("out".to_string(), Default::default());
+
+        // Configure fixed-output derivations if required.
+
+        async fn select_string(
+            co: &GenCo,
+            attrs: &NixAttrs,
+            key: &str,
+        ) -> Result<Option<String>, ErrorKind> {
+            if let Some(attr) = attrs.select(key) {
+                return Ok(Some(strong_coerce_to_string(co, attr.clone()).await?));
+            }
+
+            Ok(None)
+        }
+
+        for (name, value) in input.clone().into_iter_sorted() {
+            let value = generators::request_force(&co, value).await;
+            if ignore_nulls && matches!(value, Value::Null) {
+                continue;
+            }
+
+            let val_str = strong_coerce_to_string(&co, value.clone()).await?;
+
+            // handle_derivation_parameters tells us whether the
+            // argument should be added to the environment; continue
+            // to the next one otherwise
+            if !handle_derivation_parameters(&mut drv, &co, name.as_str(), &value, &val_str).await?
+            {
+                continue;
+            }
+
+            // Most of these are also added to the builder's environment in "raw" form.
+            if drv
+                .environment
+                .insert(name.as_str().to_string(), val_str)
+                .is_some()
+            {
+                return Err(Error::DuplicateEnvVar(name.as_str().to_string()).into());
+            }
+        }
+
+        populate_output_configuration(
+            &mut drv,
+            select_string(&co, &input, "outputHash")
+                .await
+                .context("evaluating the `outputHash` parameter")?,
+            select_string(&co, &input, "outputHashAlgo")
+                .await
+                .context("evaluating the `outputHashAlgo` parameter")?,
+            select_string(&co, &input, "outputHashMode")
+                .await
+                .context("evaluating the `outputHashMode` parameter")?,
+        )?;
+
+        // Scan references in relevant attributes to detect any build-references.
+        let references = {
+            let state = state.borrow();
+            if state.is_empty() {
+                // skip reference scanning, create an empty result
+                Default::default()
+            } else {
+                let mut refscan = state.reference_scanner();
+                drv.arguments.iter().for_each(|s| refscan.scan_str(s));
+                drv.environment.values().for_each(|s| refscan.scan_str(s));
+                refscan.scan_str(&drv.builder);
+                refscan.finalise()
+            }
+        };
+
+        // Each output name needs to exist in the environment, at this
+        // point initialised as an empty string because that is the
+        // way of Golang ;)
+        for output in drv.outputs.keys() {
+            if drv
+                .environment
+                .insert(output.to_string(), String::new())
+                .is_some()
+            {
+                return Err(Error::ShadowedOutput(output.to_string()).into());
+            }
+        }
+
+        let mut known_paths = state.borrow_mut();
+        populate_inputs(&mut drv, &known_paths, references);
+
+        // At this point, derivation fields are fully populated from
+        // eval data structures.
+        drv.validate(false).map_err(Error::InvalidDerivation)?;
+
+        // Calculate the derivation_or_fod_hash for the current derivation.
+        // This one is still intermediate (so not added to known_paths)
+        let derivation_or_fod_hash_tmp =
+            drv.derivation_or_fod_hash(|drv| known_paths.get_hash_derivation_modulo(drv));
+
+        // Mutate the Derivation struct and set output paths
+        drv.calculate_output_paths(&name, &derivation_or_fod_hash_tmp)
+            .map_err(Error::InvalidDerivation)?;
+
+        let derivation_path = drv
+            .calculate_derivation_path(&name)
+            .map_err(Error::InvalidDerivation)?;
+
+        // recompute the hash derivation modulo and add to known_paths
+        let derivation_or_fod_hash_final =
+            drv.derivation_or_fod_hash(|drv| known_paths.get_hash_derivation_modulo(drv));
+
+        known_paths.add_hash_derivation_modulo(
+            derivation_path.to_absolute_path(),
+            &derivation_or_fod_hash_final,
+        );
+
+        // mark all the new paths as known
+        let output_names: Vec<String> = drv.outputs.keys().map(Clone::clone).collect();
+        known_paths.drv(derivation_path.to_absolute_path(), &output_names);
+
+        for (output_name, output) in &drv.outputs {
+            known_paths.output(
+                &output.path,
+                output_name,
+                derivation_path.to_absolute_path(),
+            );
+        }
+
+        let mut new_attrs: Vec<(String, String)> = drv
+            .outputs
+            .into_iter()
+            .map(|(name, output)| (name, output.path))
+            .collect();
+
+        new_attrs.push(("drvPath".to_string(), derivation_path.to_absolute_path()));
+
+        Ok(Value::Attrs(Box::new(NixAttrs::from_iter(
+            new_attrs.into_iter(),
+        ))))
+    }
+
+    #[builtin("toFile")]
+    async fn builtin_to_file(
+        state: Rc<RefCell<KnownPaths>>,
+        co: GenCo,
+        name: Value,
+        content: Value,
+    ) -> Result<Value, ErrorKind> {
+        let name = name
+            .to_str()
+            .context("evaluating the `name` parameter of builtins.toFile")?;
+        let content = content
+            .to_str()
+            .context("evaluating the `content` parameter of builtins.toFile")?;
+
+        let mut refscan = state.borrow().reference_scanner();
+        refscan.scan_str(content.as_str());
+        let refs = {
+            let paths = state.borrow();
+            refscan
+                .finalise()
+                .into_iter()
+                .map(|path| paths[&path].path.to_string())
+                .collect::<Vec<_>>()
+        };
+
+        // TODO: fail on derivation references (only "plain" is allowed here)
+
+        let path = nix_compat::store_path::build_text_path(name.as_str(), content.as_str(), refs)
+            .map_err(|_e| {
+                nix_compat::derivation::DerivationError::InvalidOutputName(
+                    name.as_str().to_string(),
+                )
+            })
+            .map_err(Error::InvalidDerivation)?
+            .to_absolute_path();
+
+        state.borrow_mut().plain(&path);
+
+        // TODO: actually persist the file in the store at that path ...
+
+        Ok(Value::String(path.into()))
+    }
+}
+
+pub use derivation_builtins::builtins as derivation_builtins;
+
+#[cfg(test)]
+mod tests {
+    use nix_compat::store_path::hash_placeholder;
+    use tvix_eval::observer::NoOpObserver;
+
+    // TODO: These tests are commented out because we do not have
+    // scaffolding to drive generators during testing at the moment.
+
+    // static mut OBSERVER: NoOpObserver = NoOpObserver {};
+
+    // // Creates a fake VM for tests, which can *not* actually be
+    // // used to force (most) values but can satisfy the type
+    // // parameter.
+    // fn fake_vm() -> VM<'static> {
+    //     // safe because accessing the observer doesn't actually do anything
+    //     unsafe {
+    //         VM::new(
+    //             Default::default(),
+    //             Box::new(tvix_eval::DummyIO),
+    //             &mut OBSERVER,
+    //             Default::default(),
+    //             todo!(),
+    //         )
+    //     }
+    // }
+
+    // #[test]
+    // fn populate_outputs_ok() {
+    //     let mut vm = fake_vm();
+    //     let mut drv = Derivation::default();
+    //     drv.outputs.insert("out".to_string(), Default::default());
+
+    //     let outputs = NixList::construct(
+    //         2,
+    //         vec![Value::String("foo".into()), Value::String("bar".into())],
+    //     );
+
+    //     populate_outputs(&mut vm, &mut drv, outputs).expect("populate_outputs should succeed");
+
+    //     assert_eq!(drv.outputs.len(), 2);
+    //     assert!(drv.outputs.contains_key("bar"));
+    //     assert!(drv.outputs.contains_key("foo"));
+    // }
+
+    // #[test]
+    // fn populate_outputs_duplicate() {
+    //     let mut vm = fake_vm();
+    //     let mut drv = Derivation::default();
+    //     drv.outputs.insert("out".to_string(), Default::default());
+
+    //     let outputs = NixList::construct(
+    //         2,
+    //         vec![Value::String("foo".into()), Value::String("foo".into())],
+    //     );
+
+    //     populate_outputs(&mut vm, &mut drv, outputs)
+    //         .expect_err("supplying duplicate outputs should fail");
+    // }
+
+    // #[test]
+    // fn populate_inputs_empty() {
+    //     let mut drv = Derivation::default();
+    //     let paths = KnownPaths::default();
+    //     let inputs = vec![];
+
+    //     populate_inputs(&mut drv, &paths, inputs);
+
+    //     assert!(drv.input_sources.is_empty());
+    //     assert!(drv.input_derivations.is_empty());
+    // }
+
+    // #[test]
+    // fn populate_inputs_all() {
+    //     let mut drv = Derivation::default();
+
+    //     let mut paths = KnownPaths::default();
+    //     paths.plain("/nix/store/fn7zvafq26f0c8b17brs7s95s10ibfzs-foo");
+    //     paths.drv(
+    //         "/nix/store/aqffiyqx602lbam7n1zsaz3yrh6v08pc-bar.drv",
+    //         &["out"],
+    //     );
+    //     paths.output(
+    //         "/nix/store/zvpskvjwi72fjxg0vzq822sfvq20mq4l-bar",
+    //         "out",
+    //         "/nix/store/aqffiyqx602lbam7n1zsaz3yrh6v08pc-bar.drv",
+    //     );
+
+    //     let inputs = vec![
+    //         "/nix/store/fn7zvafq26f0c8b17brs7s95s10ibfzs-foo".into(),
+    //         "/nix/store/aqffiyqx602lbam7n1zsaz3yrh6v08pc-bar.drv".into(),
+    //         "/nix/store/zvpskvjwi72fjxg0vzq822sfvq20mq4l-bar".into(),
+    //     ];
+
+    //     populate_inputs(&mut drv, &paths, inputs);
+
+    //     assert_eq!(drv.input_sources.len(), 1);
+    //     assert!(drv
+    //         .input_sources
+    //         .contains("/nix/store/fn7zvafq26f0c8b17brs7s95s10ibfzs-foo"));
+
+    //     assert_eq!(drv.input_derivations.len(), 1);
+    //     assert!(drv
+    //         .input_derivations
+    //         .contains_key("/nix/store/aqffiyqx602lbam7n1zsaz3yrh6v08pc-bar.drv"));
+    // }
+
+    // #[test]
+    // fn populate_output_config_std() {
+    //     let mut drv = Derivation::default();
+
+    //     populate_output_configuration(&mut drv, None, None, None)
+    //         .expect("populate_output_configuration() should succeed");
+
+    //     assert_eq!(drv, Derivation::default(), "derivation should be unchanged");
+    // }
+
+    // #[test]
+    // fn populate_output_config_fod() {
+    //     let mut drv = Derivation::default();
+    //     drv.outputs.insert("out".to_string(), Default::default());
+
+    //     populate_output_configuration(
+    //         &mut drv,
+    //         Some("0000000000000000000000000000000000000000000000000000000000000000".into()),
+    //         Some("sha256".into()),
+    //         None,
+    //     )
+    //     .expect("populate_output_configuration() should succeed");
+
+    //     let expected = Hash {
+    //         algo: "sha256".into(),
+    //         digest: "0000000000000000000000000000000000000000000000000000000000000000".into(),
+    //     };
+
+    //     assert_eq!(drv.outputs["out"].hash, Some(expected));
+    // }
+
+    // #[test]
+    // fn populate_output_config_fod_recursive() {
+    //     let mut drv = Derivation::default();
+    //     drv.outputs.insert("out".to_string(), Default::default());
+
+    //     populate_output_configuration(
+    //         &mut drv,
+    //         Some("0000000000000000000000000000000000000000000000000000000000000000".into()),
+    //         Some("sha256".into()),
+    //         Some("recursive".into()),
+    //     )
+    //     .expect("populate_output_configuration() should succeed");
+
+    //     let expected = Hash {
+    //         algo: "r:sha256".into(),
+    //         digest: "0000000000000000000000000000000000000000000000000000000000000000".into(),
+    //     };
+
+    //     assert_eq!(drv.outputs["out"].hash, Some(expected));
+    // }
+
+    // #[test]
+    // /// hash_algo set to sha256, but SRI hash passed
+    // fn populate_output_config_flat_sri_sha256() {
+    //     let mut drv = Derivation::default();
+    //     drv.outputs.insert("out".to_string(), Default::default());
+
+    //     populate_output_configuration(
+    //         &mut drv,
+    //         Some("sha256-swapHA/ZO8QoDPwumMt6s5gf91oYe+oyk4EfRSyJqMg=".into()),
+    //         Some("sha256".into()),
+    //         Some("flat".into()),
+    //     )
+    //     .expect("populate_output_configuration() should succeed");
+
+    //     let expected = Hash {
+    //         algo: "sha256".into(),
+    //         digest: "b306a91c0fd93bc4280cfc2e98cb7ab3981ff75a187bea3293811f452c89a8c8".into(), // lower hex
+    //     };
+
+    //     assert_eq!(drv.outputs["out"].hash, Some(expected));
+    // }
+
+    // #[test]
+    // /// hash_algo set to empty string, SRI hash passed
+    // fn populate_output_config_flat_sri() {
+    //     let mut drv = Derivation::default();
+    //     drv.outputs.insert("out".to_string(), Default::default());
+
+    //     populate_output_configuration(
+    //         &mut drv,
+    //         Some("sha256-s6JN6XqP28g1uYMxaVAQMLiXcDG8tUs7OsE3QPhGqzA=".into()),
+    //         Some("".into()),
+    //         Some("flat".into()),
+    //     )
+    //     .expect("populate_output_configuration() should succeed");
+
+    //     let expected = Hash {
+    //         algo: "sha256".into(),
+    //         digest: "b3a24de97a8fdbc835b9833169501030b8977031bcb54b3b3ac13740f846ab30".into(), // lower hex
+    //     };
+
+    //     assert_eq!(drv.outputs["out"].hash, Some(expected));
+    // }
+
+    // #[test]
+    // fn handle_outputs_parameter() {
+    //     let mut vm = fake_vm();
+    //     let mut drv = Derivation::default();
+    //     drv.outputs.insert("out".to_string(), Default::default());
+
+    //     let outputs = Value::List(NixList::construct(
+    //         2,
+    //         vec![Value::String("foo".into()), Value::String("bar".into())],
+    //     ));
+    //     let outputs_str = outputs
+    //         .coerce_to_string(CoercionKind::Strong, &mut vm)
+    //         .unwrap();
+
+    //     handle_derivation_parameters(&mut drv, &mut vm, "outputs", &outputs, outputs_str.as_str())
+    //         .expect("handling 'outputs' parameter should succeed");
+
+    //     assert_eq!(drv.outputs.len(), 2);
+    //     assert!(drv.outputs.contains_key("bar"));
+    //     assert!(drv.outputs.contains_key("foo"));
+    // }
+
+    // #[test]
+    // fn handle_args_parameter() {
+    //     let mut vm = fake_vm();
+    //     let mut drv = Derivation::default();
+
+    //     let args = Value::List(NixList::construct(
+    //         3,
+    //         vec![
+    //             Value::String("--foo".into()),
+    //             Value::String("42".into()),
+    //             Value::String("--bar".into()),
+    //         ],
+    //     ));
+
+    //     let args_str = args
+    //         .coerce_to_string(CoercionKind::Strong, &mut vm)
+    //         .unwrap();
+
+    //     handle_derivation_parameters(&mut drv, &mut vm, "args", &args, args_str.as_str())
+    //         .expect("handling 'args' parameter should succeed");
+
+    //     assert_eq!(
+    //         drv.arguments,
+    //         vec!["--foo".to_string(), "42".to_string(), "--bar".to_string()]
+    //     );
+    // }
+
+    #[test]
+    fn builtins_placeholder_hashes() {
+        assert_eq!(
+            hash_placeholder("out").as_str(),
+            "/1rz4g4znpzjwh1xymhjpm42vipw92pr73vdgl6xs1hycac8kf2n9"
+        );
+
+        assert_eq!(
+            hash_placeholder("").as_str(),
+            "/171rf4jhx57xqz3p7swniwkig249cif71pa08p80mgaf0mqz5bmr"
+        );
+    }
+}
diff --git a/tvix/cli/src/errors.rs b/tvix/cli/src/errors.rs
new file mode 100644
index 000000000000..6f328dc0003c
--- /dev/null
+++ b/tvix/cli/src/errors.rs
@@ -0,0 +1,28 @@
+use nix_compat::{derivation::DerivationError, nixhash};
+use std::rc::Rc;
+use thiserror::Error;
+
+/// Errors related to derivation construction
+#[derive(Debug, Error)]
+pub enum Error {
+    #[error("an output with the name '{0}' is already defined")]
+    DuplicateOutput(String),
+    #[error("fixed-output derivations can only have the default `out`-output")]
+    ConflictingOutputTypes,
+    #[error("the environment variable '{0}' has already been set in this derivation")]
+    DuplicateEnvVar(String),
+    #[error("the environment variable '{0}' shadows the name of an output")]
+    ShadowedOutput(String),
+    #[error("invalid derivation parameters: {0}")]
+    InvalidDerivation(DerivationError),
+    #[error("invalid output hash: {0}")]
+    InvalidOutputHash(nixhash::Error),
+    #[error("invalid output hash mode: '{0}', only 'recursive' and 'flat` are supported")]
+    InvalidOutputHashMode(String),
+}
+
+impl From<Error> for tvix_eval::ErrorKind {
+    fn from(err: Error) -> Self {
+        tvix_eval::ErrorKind::TvixError(Rc::new(err))
+    }
+}
diff --git a/tvix/cli/src/fetchurl.nix b/tvix/cli/src/fetchurl.nix
new file mode 100644
index 000000000000..3f182a5a319b
--- /dev/null
+++ b/tvix/cli/src/fetchurl.nix
@@ -0,0 +1,53 @@
+# SPDX-License-Identifier: LGPL-2.1
+#
+# This file is vendored from C++ Nix, as it needs to be bundled with
+# an evaluator to be able to evaluate nixpkgs.
+#
+# Source: https://github.com/NixOS/nix/blob/2.3.16/corepkgs/fetchurl.nix
+
+{ system ? "" # obsolete
+, url
+, hash ? "" # an SRI hash
+
+  # Legacy hash specification
+, md5 ? ""
+, sha1 ? ""
+, sha256 ? ""
+, sha512 ? ""
+, outputHash ? if hash != "" then hash else if sha512 != "" then sha512 else if sha1 != "" then sha1 else if md5 != "" then md5 else sha256
+, outputHashAlgo ? if hash != "" then "" else if sha512 != "" then "sha512" else if sha1 != "" then "sha1" else if md5 != "" then "md5" else "sha256"
+
+, executable ? false
+, unpack ? false
+, name ? baseNameOf (toString url)
+}:
+
+derivation {
+  builder = "builtin:fetchurl";
+
+  # New-style output content requirements.
+  inherit outputHashAlgo outputHash;
+  outputHashMode = if unpack || executable then "recursive" else "flat";
+
+  inherit name url executable unpack;
+
+  system = "builtin";
+
+  # No need to double the amount of network traffic
+  preferLocalBuild = true;
+
+  impureEnvVars = [
+    # We borrow these environment variables from the caller to allow
+    # easy proxy configuration.  This is impure, but a fixed-output
+    # derivation like fetchurl is allowed to do so since its result is
+    # by definition pure.
+    "http_proxy"
+    "https_proxy"
+    "ftp_proxy"
+    "all_proxy"
+    "no_proxy"
+  ];
+
+  # To make "nix-prefetch-url" work.
+  urls = [ url ];
+}
diff --git a/tvix/cli/src/known_paths.rs b/tvix/cli/src/known_paths.rs
new file mode 100644
index 000000000000..07373ef0da7a
--- /dev/null
+++ b/tvix/cli/src/known_paths.rs
@@ -0,0 +1,186 @@
+//! This module implements logic required for persisting known paths
+//! during an evaluation.
+//!
+//! Tvix needs to be able to keep track of each Nix store path that it
+//! knows about during the scope of a single evaluation and its
+//! related builds.
+//!
+//! This data is required to scan derivation inputs for the build
+//! references (the "build closure") that they make use of.
+//!
+//! Please see //tvix/eval/docs/build-references.md for more
+//! information.
+
+use crate::refscan::{ReferenceScanner, STORE_PATH_LEN};
+use nix_compat::nixhash::NixHash;
+use std::{
+    collections::{hash_map, BTreeSet, HashMap},
+    ops::Index,
+};
+
+#[derive(Debug, PartialEq)]
+pub enum PathKind {
+    /// A literal derivation (`.drv`-file), and the *names* of its outputs.
+    Derivation { output_names: BTreeSet<String> },
+
+    /// An output of a derivation, its name, and the path of its derivation.
+    Output { name: String, derivation: String },
+
+    /// A plain store path (e.g. source files copied to the store).
+    Plain,
+}
+
+#[derive(Debug, PartialEq)]
+pub struct KnownPath {
+    pub path: String,
+    pub kind: PathKind,
+}
+
+impl KnownPath {
+    fn new(path: String, kind: PathKind) -> Self {
+        KnownPath { path, kind }
+    }
+}
+
+/// Internal struct to prevent accidental leaks of the truncated path
+/// names.
+#[repr(transparent)]
+#[derive(Clone, Debug, Default, PartialEq, PartialOrd, Ord, Eq, Hash)]
+pub struct PathName(String);
+
+impl From<&str> for PathName {
+    fn from(s: &str) -> Self {
+        PathName(s[..STORE_PATH_LEN].to_string())
+    }
+}
+
+/// This instance is required to pass PathName instances as needles to
+/// the reference scanner.
+impl AsRef<[u8]> for PathName {
+    fn as_ref(&self) -> &[u8] {
+        self.0.as_ref()
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct KnownPaths {
+    /// All known paths, keyed by a truncated version of their store
+    /// path used for reference scanning.
+    paths: HashMap<PathName, KnownPath>,
+
+    /// All known derivation or FOD hashes.
+    ///
+    /// Keys are derivation paths, values is the NixHash.
+    derivation_or_fod_hashes: HashMap<String, NixHash>,
+}
+
+impl Index<&PathName> for KnownPaths {
+    type Output = KnownPath;
+
+    fn index(&self, index: &PathName) -> &Self::Output {
+        &self.paths[index]
+    }
+}
+
+impl KnownPaths {
+    fn insert_path(&mut self, path: String, path_kind: PathKind) {
+        match self.paths.entry(path.as_str().into()) {
+            hash_map::Entry::Vacant(entry) => {
+                entry.insert(KnownPath::new(path, path_kind));
+            }
+
+            hash_map::Entry::Occupied(mut entry) => {
+                match (path_kind, &mut entry.get_mut().kind) {
+                    // These variant combinations require no "merging action".
+                    (PathKind::Plain, PathKind::Plain) => (),
+                    (PathKind::Output { .. }, PathKind::Output { .. }) => (),
+
+                    (
+                        PathKind::Derivation { output_names: new },
+                        PathKind::Derivation {
+                            output_names: ref mut old,
+                        },
+                    ) => {
+                        old.extend(new);
+                    }
+
+                    _ => panic!(
+                        "path '{}' inserted twice with different types",
+                        entry.key().0
+                    ),
+                };
+            }
+        };
+    }
+
+    /// Mark a plain path as known.
+    pub fn plain<S: ToString>(&mut self, path: S) {
+        self.insert_path(path.to_string(), PathKind::Plain);
+    }
+
+    /// Mark a derivation as known.
+    pub fn drv<P: ToString, O: ToString>(&mut self, path: P, outputs: &[O]) {
+        self.insert_path(
+            path.to_string(),
+            PathKind::Derivation {
+                output_names: outputs.iter().map(ToString::to_string).collect(),
+            },
+        );
+    }
+
+    /// Mark a derivation output path as known.
+    pub fn output<P: ToString, N: ToString, D: ToString>(
+        &mut self,
+        output_path: P,
+        name: N,
+        drv_path: D,
+    ) {
+        self.insert_path(
+            output_path.to_string(),
+            PathKind::Output {
+                name: name.to_string(),
+                derivation: drv_path.to_string(),
+            },
+        );
+    }
+
+    /// Checks whether there are any known paths. If not, a reference
+    /// scanner can not be created.
+    pub fn is_empty(&self) -> bool {
+        self.paths.is_empty()
+    }
+
+    /// Create a reference scanner from the current set of known paths.
+    pub fn reference_scanner(&self) -> ReferenceScanner<PathName> {
+        let candidates = self.paths.keys().map(Clone::clone).collect();
+        ReferenceScanner::new(candidates)
+    }
+
+    /// Fetch the opaque "hash derivation modulo" for a given derivation path.
+    pub fn get_hash_derivation_modulo(&self, drv_path: &str) -> NixHash {
+        // TODO: we rely on an invariant that things *should* have
+        // been calculated if we get this far.
+        self.derivation_or_fod_hashes[drv_path].clone()
+    }
+
+    pub fn add_hash_derivation_modulo<D: ToString>(
+        &mut self,
+        drv: D,
+        hash_derivation_modulo: &NixHash,
+    ) {
+        #[allow(unused_variables)] // assertions on this only compiled in debug builds
+        let old = self
+            .derivation_or_fod_hashes
+            .insert(drv.to_string(), hash_derivation_modulo.to_owned());
+
+        #[cfg(debug_assertions)]
+        {
+            if let Some(old) = old {
+                debug_assert!(
+                    old == *hash_derivation_modulo,
+                    "hash derivation modulo for a given derivation should always be calculated the same"
+                );
+            }
+        }
+    }
+}
diff --git a/tvix/cli/src/main.rs b/tvix/cli/src/main.rs
new file mode 100644
index 000000000000..28b75dd907aa
--- /dev/null
+++ b/tvix/cli/src/main.rs
@@ -0,0 +1,284 @@
+mod derivation;
+mod errors;
+mod known_paths;
+mod nix_compat;
+mod refscan;
+mod tvix_io;
+
+use std::cell::RefCell;
+use std::rc::Rc;
+use std::{fs, path::PathBuf};
+
+use clap::Parser;
+use known_paths::KnownPaths;
+use rustyline::{error::ReadlineError, Editor};
+use tvix_eval::observer::{DisassemblingObserver, TracingObserver};
+use tvix_eval::{Builtin, Value};
+use tvix_store::blobservice::MemoryBlobService;
+use tvix_store::directoryservice::MemoryDirectoryService;
+use tvix_store::pathinfoservice::MemoryPathInfoService;
+
+#[derive(Parser)]
+struct Args {
+    /// Path to a script to evaluate
+    script: Option<PathBuf>,
+
+    #[clap(long, short = 'E')]
+    expr: Option<String>,
+
+    /// Dump the raw AST to stdout before interpreting
+    #[clap(long, env = "TVIX_DISPLAY_AST")]
+    display_ast: bool,
+
+    /// Dump the bytecode to stdout before evaluating
+    #[clap(long, env = "TVIX_DUMP_BYTECODE")]
+    dump_bytecode: bool,
+
+    /// Trace the runtime of the VM
+    #[clap(long, env = "TVIX_TRACE_RUNTIME")]
+    trace_runtime: bool,
+
+    /// Only compile, but do not execute code. This will make Tvix act
+    /// sort of like a linter.
+    #[clap(long)]
+    compile_only: bool,
+
+    /// Don't print warnings.
+    #[clap(long)]
+    no_warnings: bool,
+
+    /// A colon-separated list of directories to use to resolve `<...>`-style paths
+    #[clap(long, short = 'I', env = "NIX_PATH")]
+    nix_search_path: Option<String>,
+
+    /// Print "raw" (unquoted) output.
+    #[clap(long)]
+    raw: bool,
+
+    /// Strictly evaluate values, traversing them and forcing e.g.
+    /// elements of lists and attribute sets before printing the
+    /// return value.
+    #[clap(long)]
+    strict: bool,
+}
+
+/// Interprets the given code snippet, printing out warnings, errors
+/// and the result itself. The return value indicates whether
+/// evaluation succeeded.
+fn interpret(code: &str, path: Option<PathBuf>, args: &Args, explain: bool) -> bool {
+    let mut eval = tvix_eval::Evaluation::new_impure(code, path);
+    let known_paths: Rc<RefCell<KnownPaths>> = Default::default();
+
+    eval.strict = args.strict;
+
+    let blob_service = MemoryBlobService::default();
+    let directory_service = MemoryDirectoryService::default();
+    let path_info_service = MemoryPathInfoService::default();
+    let nar_calculation_service = tvix_store::nar::NonCachingNARCalculationService::new(
+        blob_service.clone(),
+        directory_service.clone(),
+    );
+
+    eval.io_handle = Box::new(tvix_io::TvixIO::new(
+        known_paths.clone(),
+        tvix_store::TvixStoreIO::new(
+            blob_service,
+            directory_service,
+            path_info_service,
+            nar_calculation_service,
+        ),
+    ));
+
+    // bundle fetchurl.nix (used in nixpkgs) by resolving <nix> to
+    // `/__corepkgs__`, which has special handling in [`nix_compat`].
+    eval.nix_path = args
+        .nix_search_path
+        .as_ref()
+        .map(|p| format!("nix=/__corepkgs__:{}", p))
+        .or_else(|| Some("nix=/__corepkgs__".to_string()));
+
+    eval.builtins
+        .extend(derivation::derivation_builtins(known_paths));
+
+    // Add the actual `builtins.derivation` from compiled Nix code
+    eval.src_builtins
+        .push(("derivation", include_str!("derivation.nix")));
+
+    let source_map = eval.source_map();
+    let result = {
+        let mut compiler_observer =
+            DisassemblingObserver::new(source_map.clone(), std::io::stderr());
+        if args.dump_bytecode {
+            eval.compiler_observer = Some(&mut compiler_observer);
+        }
+
+        let mut runtime_observer = TracingObserver::new(std::io::stderr());
+        if args.trace_runtime {
+            eval.runtime_observer = Some(&mut runtime_observer);
+        }
+
+        eval.evaluate()
+    };
+
+    if args.display_ast {
+        if let Some(ref expr) = result.expr {
+            eprintln!("AST: {}", tvix_eval::pretty_print_expr(expr));
+        }
+    }
+
+    for error in &result.errors {
+        error.fancy_format_stderr(&source_map);
+    }
+
+    if !args.no_warnings {
+        for warning in &result.warnings {
+            warning.fancy_format_stderr(&source_map);
+        }
+    }
+
+    if let Some(value) = result.value.as_ref() {
+        if explain {
+            println!("=> {}", value.explain());
+        } else {
+            println_result(value, args.raw);
+        }
+    }
+
+    // inform the caller about any errors
+    result.errors.is_empty()
+}
+
+/// Interpret the given code snippet, but only run the Tvix compiler
+/// on it and return errors and warnings.
+fn lint(code: &str, path: Option<PathBuf>, args: &Args) -> bool {
+    let mut eval = tvix_eval::Evaluation::new_impure(code, path);
+    eval.strict = args.strict;
+
+    let source_map = eval.source_map();
+
+    let mut compiler_observer = DisassemblingObserver::new(source_map.clone(), std::io::stderr());
+
+    if args.dump_bytecode {
+        eval.compiler_observer = Some(&mut compiler_observer);
+    }
+
+    if args.trace_runtime {
+        eprintln!("warning: --trace-runtime has no effect with --compile-only!");
+    }
+
+    let result = eval.compile_only();
+
+    if args.display_ast {
+        if let Some(ref expr) = result.expr {
+            eprintln!("AST: {}", tvix_eval::pretty_print_expr(expr));
+        }
+    }
+
+    for error in &result.errors {
+        error.fancy_format_stderr(&source_map);
+    }
+
+    for warning in &result.warnings {
+        warning.fancy_format_stderr(&source_map);
+    }
+
+    // inform the caller about any errors
+    result.errors.is_empty()
+}
+
+fn main() {
+    let args = Args::parse();
+
+    if let Some(file) = &args.script {
+        run_file(file.clone(), &args)
+    } else if let Some(expr) = &args.expr {
+        if !interpret(expr, None, &args, false) {
+            std::process::exit(1);
+        }
+    } else {
+        run_prompt(&args)
+    }
+}
+
+fn run_file(mut path: PathBuf, args: &Args) {
+    if path.is_dir() {
+        path.push("default.nix");
+    }
+    let contents = fs::read_to_string(&path).expect("failed to read the input file");
+
+    let success = if args.compile_only {
+        lint(&contents, Some(path), args)
+    } else {
+        interpret(&contents, Some(path), args, false)
+    };
+
+    if !success {
+        std::process::exit(1);
+    }
+}
+
+fn println_result(result: &Value, raw: bool) {
+    if raw {
+        println!("{}", result.to_str().unwrap().as_str())
+    } else {
+        println!("=> {} :: {}", result, result.type_of())
+    }
+}
+
+fn state_dir() -> Option<PathBuf> {
+    let mut path = dirs::data_dir();
+    if let Some(p) = path.as_mut() {
+        p.push("tvix")
+    }
+    path
+}
+
+fn run_prompt(args: &Args) {
+    let mut rl = Editor::<()>::new().expect("should be able to launch rustyline");
+
+    if args.compile_only {
+        eprintln!("warning: `--compile-only` has no effect on REPL usage!");
+    }
+
+    let history_path = match state_dir() {
+        // Attempt to set up these paths, but do not hard fail if it
+        // doesn't work.
+        Some(mut path) => {
+            let _ = std::fs::create_dir_all(&path);
+            path.push("history.txt");
+            let _ = rl.load_history(&path);
+            Some(path)
+        }
+
+        None => None,
+    };
+
+    loop {
+        let readline = rl.readline("tvix-repl> ");
+        match readline {
+            Ok(line) => {
+                if line.is_empty() {
+                    continue;
+                }
+
+                rl.add_history_entry(&line);
+
+                if let Some(without_prefix) = line.strip_prefix(":d ") {
+                    interpret(without_prefix, None, args, true);
+                } else {
+                    interpret(&line, None, args, false);
+                }
+            }
+            Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => break,
+
+            Err(err) => {
+                eprintln!("error: {}", err);
+                break;
+            }
+        }
+    }
+
+    if let Some(path) = history_path {
+        rl.save_history(&path).unwrap();
+    }
+}
diff --git a/tvix/cli/src/nix_compat.rs b/tvix/cli/src/nix_compat.rs
new file mode 100644
index 000000000000..bda238983839
--- /dev/null
+++ b/tvix/cli/src/nix_compat.rs
@@ -0,0 +1,122 @@
+//! This module implements (temporary) compatibility shims between
+//! Tvix and C++ Nix.
+//!
+//! These are not intended to be long-lived, but should bootstrap Tvix
+//! by piggybacking off functionality that already exists in Nix and
+//! is still being implemented in Tvix.
+
+use std::collections::HashMap;
+use std::path::Path;
+use std::process::Command;
+use std::sync::RwLock;
+use std::{io, path::PathBuf};
+
+use smol_str::SmolStr;
+use tvix_eval::{EvalIO, FileType, StdIO};
+
+/// Compatibility implementation of [`EvalIO`] that uses C++ Nix to
+/// write files to the Nix store.
+pub struct NixCompatIO {
+    /// Most IO requests are tunneled through to [`tvix_eval::StdIO`]
+    /// instead.
+    underlying: StdIO,
+
+    /// Cache paths for identical files being imported to the store.
+    // TODO(tazjin): This could be done better by having a thunk cache
+    // for these calls on the eval side, but that is a little more
+    // complex.
+    import_cache: RwLock<HashMap<PathBuf, PathBuf>>,
+}
+
+impl EvalIO for NixCompatIO {
+    fn store_dir(&self) -> Option<String> {
+        Some("/nix/store".into())
+    }
+
+    // Pass path imports through to `nix-store --add`
+    fn import_path(&self, path: &Path) -> Result<PathBuf, io::Error> {
+        let path = path.to_owned();
+        if let Some(path) = self
+            .import_cache
+            .read()
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?
+            .get(&path)
+        {
+            return Ok(path.to_path_buf());
+        }
+
+        let store_path = self.add_to_store(&path)?;
+
+        self.import_cache
+            .write()
+            .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?
+            .insert(path, store_path.clone());
+
+        Ok(store_path)
+    }
+
+    // Pass the rest of the functions through to `Self::underlying`
+    fn path_exists(&self, path: &Path) -> Result<bool, io::Error> {
+        if path.starts_with("/__corepkgs__") {
+            return Ok(true);
+        }
+
+        self.underlying.path_exists(path)
+    }
+
+    fn read_to_string(&self, path: &Path) -> Result<String, io::Error> {
+        // Bundled version of corepkgs/fetchurl.nix. This workaround
+        // is similar to what cppnix does for passing the path
+        // through.
+        //
+        // TODO: this comparison is bad and allocates, we should use
+        // the sane path library.
+        if path.starts_with("/__corepkgs__/fetchurl.nix") {
+            return Ok(include_str!("fetchurl.nix").to_string());
+        }
+
+        self.underlying.read_to_string(path)
+    }
+
+    fn read_dir(&self, path: &Path) -> Result<Vec<(SmolStr, FileType)>, io::Error> {
+        self.underlying.read_dir(path)
+    }
+}
+
+impl NixCompatIO {
+    pub fn new() -> Self {
+        NixCompatIO {
+            underlying: StdIO,
+            import_cache: RwLock::new(HashMap::new()),
+        }
+    }
+
+    /// Add a path to the Nix store using the `nix-store --add`
+    /// functionality from C++ Nix.
+    fn add_to_store(&self, path: &Path) -> Result<PathBuf, io::Error> {
+        if !path.try_exists()? {
+            return Err(io::Error::from(io::ErrorKind::NotFound));
+        }
+
+        let mut cmd = Command::new("nix-store");
+        cmd.arg("--add");
+        cmd.arg(path);
+
+        let out = cmd.output()?;
+
+        if !out.status.success() {
+            return Err(io::Error::new(
+                io::ErrorKind::Other,
+                String::from_utf8_lossy(&out.stderr).trim().to_owned(),
+            ));
+        }
+
+        let out_path_str = String::from_utf8(out.stdout)
+            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
+        let out_path_trimmed = out_path_str.trim();
+
+        let mut out_path = PathBuf::new();
+        out_path.push(out_path_trimmed);
+        Ok(out_path)
+    }
+}
diff --git a/tvix/cli/src/refscan.rs b/tvix/cli/src/refscan.rs
new file mode 100644
index 000000000000..739cf77ed26f
--- /dev/null
+++ b/tvix/cli/src/refscan.rs
@@ -0,0 +1,115 @@
+//! Simple scanner for non-overlapping, known references of Nix store paths in a
+//! given string.
+//!
+//! This is used for determining build references (see
+//! //tvix/eval/docs/build-references.md for more details).
+//!
+//! The scanner itself is an Aho-Corasick automaton, using the `aho-corasick`
+//! crate.
+
+use std::collections::BTreeSet;
+use wu_manber::TwoByteWM;
+
+pub const STORE_PATH_LEN: usize = "/nix/store/00000000000000000000000000000000".len();
+
+/// Represents a "primed" reference scanner with an automaton that knows the set
+/// of store paths to scan for.
+pub struct ReferenceScanner<P: Ord + AsRef<[u8]>> {
+    candidates: Vec<P>,
+    searcher: Option<TwoByteWM>,
+    matches: Vec<usize>,
+}
+
+impl<P: Clone + Ord + AsRef<[u8]>> ReferenceScanner<P> {
+    /// Construct a new `ReferenceScanner` that knows how to scan for the given
+    /// candidate store paths.
+    pub fn new(candidates: Vec<P>) -> Self {
+        let searcher = if candidates.is_empty() {
+            None
+        } else {
+            Some(TwoByteWM::new(&candidates))
+        };
+
+        ReferenceScanner {
+            searcher,
+            candidates,
+            matches: Default::default(),
+        }
+    }
+
+    /// Scan the given string for all non-overlapping matches and collect them
+    /// in the scanner.
+    pub fn scan_str(&mut self, haystack: &str) {
+        if haystack.len() < STORE_PATH_LEN {
+            return;
+        }
+
+        if let Some(searcher) = &self.searcher {
+            for m in searcher.find(&haystack) {
+                self.matches.push(m.pat_idx);
+            }
+        }
+    }
+
+    /// Finalise the reference scanner and return the resulting matches.
+    pub fn finalise(self) -> BTreeSet<P> {
+        self.matches
+            .into_iter()
+            .map(|idx| self.candidates[idx].clone())
+            .collect()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // The actual derivation of `nixpkgs.hello`.
+    const HELLO_DRV: &'static str = r#"Derive([("out","/nix/store/33l4p0pn0mybmqzaxfkpppyh7vx1c74p-hello-2.12.1","","")],[("/nix/store/6z1jfnqqgyqr221zgbpm30v91yfj3r45-bash-5.1-p16.drv",["out"]),("/nix/store/ap9g09fxbicj836zm88d56dn3ff4clxl-stdenv-linux.drv",["out"]),("/nix/store/pf80kikyxr63wrw56k00i1kw6ba76qik-hello-2.12.1.tar.gz.drv",["out"])],["/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],"x86_64-linux","/nix/store/4xw8n979xpivdc46a9ndcvyhwgif00hz-bash-5.1-p16/bin/bash",["-e","/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],[("buildInputs",""),("builder","/nix/store/4xw8n979xpivdc46a9ndcvyhwgif00hz-bash-5.1-p16/bin/bash"),("cmakeFlags",""),("configureFlags",""),("depsBuildBuild",""),("depsBuildBuildPropagated",""),("depsBuildTarget",""),("depsBuildTargetPropagated",""),("depsHostHost",""),("depsHostHostPropagated",""),("depsTargetTarget",""),("depsTargetTargetPropagated",""),("doCheck","1"),("doInstallCheck",""),("mesonFlags",""),("name","hello-2.12.1"),("nativeBuildInputs",""),("out","/nix/store/33l4p0pn0mybmqzaxfkpppyh7vx1c74p-hello-2.12.1"),("outputs","out"),("patches",""),("pname","hello"),("propagatedBuildInputs",""),("propagatedNativeBuildInputs",""),("src","/nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz"),("stdenv","/nix/store/cp65c8nk29qq5cl1wyy5qyw103cwmax7-stdenv-linux"),("strictDeps",""),("system","x86_64-linux"),("version","2.12.1")])"#;
+
+    #[test]
+    fn test_no_patterns() {
+        let mut scanner: ReferenceScanner<String> = ReferenceScanner::new(vec![]);
+
+        scanner.scan_str(HELLO_DRV);
+
+        let result = scanner.finalise();
+
+        assert_eq!(result.len(), 0);
+    }
+
+    #[test]
+    fn test_single_match() {
+        let mut scanner = ReferenceScanner::new(vec![
+            "/nix/store/4xw8n979xpivdc46a9ndcvyhwgif00hz-bash-5.1-p16".to_string(),
+        ]);
+        scanner.scan_str(HELLO_DRV);
+
+        let result = scanner.finalise();
+
+        assert_eq!(result.len(), 1);
+        assert!(result.contains("/nix/store/4xw8n979xpivdc46a9ndcvyhwgif00hz-bash-5.1-p16"));
+    }
+
+    #[test]
+    fn test_multiple_matches() {
+        let candidates = vec![
+            // these exist in the drv:
+            "/nix/store/33l4p0pn0mybmqzaxfkpppyh7vx1c74p-hello-2.12.1".to_string(),
+            "/nix/store/pf80kikyxr63wrw56k00i1kw6ba76qik-hello-2.12.1.tar.gz.drv".to_string(),
+            "/nix/store/cp65c8nk29qq5cl1wyy5qyw103cwmax7-stdenv-linux".to_string(),
+            // this doesn't:
+            "/nix/store/fn7zvafq26f0c8b17brs7s95s10ibfzs-emacs-28.2.drv".to_string(),
+        ];
+
+        let mut scanner = ReferenceScanner::new(candidates.clone());
+        scanner.scan_str(HELLO_DRV);
+
+        let result = scanner.finalise();
+        assert_eq!(result.len(), 3);
+
+        for c in candidates[..3].iter() {
+            assert!(result.contains(c));
+        }
+    }
+}
diff --git a/tvix/cli/src/tvix_io.rs b/tvix/cli/src/tvix_io.rs
new file mode 100644
index 000000000000..8ca660f87b05
--- /dev/null
+++ b/tvix/cli/src/tvix_io.rs
@@ -0,0 +1,79 @@
+//! This module implements a wrapper around tvix-eval's [EvalIO] type,
+//! adding functionality which is required by tvix-cli:
+//!
+//! 1. Marking plain paths known to the reference scanner.
+//! 2. Handling the C++ Nix `__corepkgs__`-hack for nixpkgs bootstrapping.
+//!
+//! All uses of [EvalIO] in tvix-cli must make use of this wrapper,
+//! otherwise fundamental features like nixpkgs bootstrapping and hash
+//! calculation will not work.
+
+use crate::KnownPaths;
+use smol_str::SmolStr;
+use std::cell::RefCell;
+use std::io;
+use std::path::{Path, PathBuf};
+use std::rc::Rc;
+use tvix_eval::{EvalIO, FileType};
+
+pub(crate) struct TvixIO<T: EvalIO> {
+    /// Ingested paths must be reported to this known paths tracker
+    /// for accurate build reference scanning.
+    known_paths: Rc<RefCell<KnownPaths>>,
+
+    // Actual underlying [EvalIO] implementation.
+    actual: T,
+}
+
+impl<T: EvalIO> TvixIO<T> {
+    pub(crate) fn new(known_paths: Rc<RefCell<KnownPaths>>, actual: T) -> Self {
+        Self {
+            known_paths,
+            actual,
+        }
+    }
+}
+
+impl<T: EvalIO> EvalIO for TvixIO<T> {
+    fn store_dir(&self) -> Option<String> {
+        self.actual.store_dir()
+    }
+
+    fn import_path(&self, path: &Path) -> Result<PathBuf, io::Error> {
+        let imported_path = self.actual.import_path(path)?;
+        self.known_paths
+            .borrow_mut()
+            .plain(imported_path.to_string_lossy());
+
+        Ok(imported_path)
+    }
+
+    fn path_exists(&self, path: &Path) -> Result<bool, io::Error> {
+        if path.starts_with("/__corepkgs__") {
+            return Ok(true);
+        }
+
+        self.actual.path_exists(path)
+    }
+
+    fn read_to_string(&self, path: &Path) -> Result<String, io::Error> {
+        // Bundled version of corepkgs/fetchurl.nix. The counterpart
+        // of this happens in `main`, where the `nix_path` of the
+        // evaluation has `nix=/__corepkgs__` added to it.
+        //
+        // This workaround is similar to what cppnix does for passing
+        // the path through.
+        //
+        // TODO: this comparison is bad and allocates, we should use
+        // the sane path library.
+        if path.starts_with("/__corepkgs__/fetchurl.nix") {
+            return Ok(include_str!("fetchurl.nix").to_string());
+        }
+
+        self.actual.read_to_string(path)
+    }
+
+    fn read_dir(&self, path: &Path) -> Result<Vec<(SmolStr, FileType)>, io::Error> {
+        self.actual.read_dir(path)
+    }
+}
diff --git a/tvix/crate-hashes.json b/tvix/crate-hashes.json
new file mode 100644
index 000000000000..80c1718ca1c1
--- /dev/null
+++ b/tvix/crate-hashes.json
@@ -0,0 +1,5 @@
+{
+  "test-generator 0.3.0 (git+https://github.com/JamesGuthrie/test-generator.git?rev=82e799979980962aec1aa324ec6e0e4cad781f41#82e799979980962aec1aa324ec6e0e4cad781f41)": "08brp3qqa55hijc7xby3lam2cc84hvx1zzfqv6lj7smlczh8k32y",
+  "tonic-mock 0.1.0 (git+https://github.com/brainrake/tonic-mock?branch=bump-dependencies#ec1a15510875de99d709d684190db5d9beab175e)": "0lwa03hpp0mxa6aa1zv5w68k61y4hccfm0q2ykyq392fwal8vb50",
+  "wu-manber 0.1.0 (git+https://github.com/tvlfyi/wu-manber.git#e77628cafcf45d41b9e455be86a6b1b9f46b2092)": "02byhfiw41mlgr1c43n2iq6jw5sbyn8l1acv5v71a07h5l18q0cy"
+}
\ No newline at end of file
diff --git a/tvix/default.nix b/tvix/default.nix
new file mode 100644
index 000000000000..ef0e2386db0b
--- /dev/null
+++ b/tvix/default.nix
@@ -0,0 +1,90 @@
+# Nix helpers for projects under //tvix
+{ pkgs, depot, ... }:
+
+let
+  # crate override for crates that need protobuf
+  protobufDep = prev: (prev.nativeBuildInputs or [ ]) ++ [ pkgs.protobuf ];
+
+  # Cargo dependencies to be used with nixpkgs rustPlatform functions.
+  cargoDeps = pkgs.rustPlatform.importCargoLock {
+    lockFile = ./Cargo.lock;
+    outputHashes = {
+      "test-generator-0.3.0" = "08brp3qqa55hijc7xby3lam2cc84hvx1zzfqv6lj7smlczh8k32y";
+      "tonic-mock-0.1.0" = "0lwa03hpp0mxa6aa1zv5w68k61y4hccfm0q2ykyq392fwal8vb50";
+      "wu-manber-0.1.0" = "02byhfiw41mlgr1c43n2iq6jw5sbyn8l1acv5v71a07h5l18q0cy";
+    };
+  };
+in
+{
+  # Load the crate2nix crate tree.
+  crates = import ./Cargo.nix {
+    inherit pkgs;
+    nixpkgs = pkgs.path;
+
+    defaultCrateOverrides = pkgs.defaultCrateOverrides // {
+      prost-build = prev: {
+        nativeBuildInputs = protobufDep prev;
+      };
+
+      tonic-reflection = prev: {
+        nativeBuildInputs = protobufDep prev;
+      };
+
+      tvix-store = prev: {
+        PROTO_ROOT = depot.tvix.store.protos;
+        nativeBuildInputs = protobufDep prev;
+      };
+    };
+  };
+
+  # Run crate2nix generate in the current working directory, then
+  # format the generated file with depotfmt.
+  crate2nixGenerate = pkgs.writeShellScriptBin "crate2nix-generate" ''
+    ${pkgs.crate2nix}/bin/crate2nix generate
+    ${depot.tools.depotfmt}/bin/depotfmt Cargo.nix
+  '';
+
+  # Provide a shell for the combined dependencies of all Tvix Rust
+  # projects. Note that as this is manually maintained it may be
+  # lacking something, but it is required for some people's workflows.
+  #
+  # This shell can be entered with e.g. `mg shell //tvix:shell`.
+  shell = pkgs.mkShell {
+    name = "tvix-rust-dev-env";
+    packages = [
+      pkgs.buf-language-server
+      pkgs.cargo
+      pkgs.clippy
+      pkgs.evans
+      pkgs.rust-analyzer
+      pkgs.rustc
+      pkgs.rustfmt
+      pkgs.protobuf
+    ];
+  };
+
+  # Build the Rust documentation for publishing on docs.tvix.dev. Currently only
+  # some crates are documented, as the crates that depend on Protobuf cause
+  # build failures.
+  rust-docs = pkgs.stdenv.mkDerivation {
+    inherit cargoDeps;
+    name = "tvix-rust-docs";
+    src = depot.third_party.gitignoreSource ./.;
+    PROTO_ROOT = depot.tvix.store.protos;
+
+    nativeBuildInputs = with pkgs; [
+      cargo
+      rust-analyzer
+      rustPlatform.cargoSetupHook
+      rustc
+      protobuf
+    ];
+
+    buildPhase = ''
+      cargo doc --document-private-items
+      mv target/doc $out
+    '';
+  };
+
+  meta.ci.targets = [ "shell" "rust-docs" ];
+}
diff --git a/tvix/docs/.gitignore b/tvix/docs/.gitignore
new file mode 100644
index 000000000000..77699ee8a3f7
--- /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 000000000000..ba9e2bdef6d3
--- /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 000000000000..5b6d79b82313
--- /dev/null
+++ b/tvix/docs/component-flow.puml
@@ -0,0 +1,60 @@
+@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
+
+        alt Build failure
+            Coord<--Build: Fail response
+            note left: It's up to the coordinator whether to exit on build failure
+        else Build success
+            Build-->Store: Push outputs to store
+            Build<--Coord: Send success & pushed response
+        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 000000000000..a7d61948c2fa
--- /dev/null
+++ b/tvix/docs/components.md
@@ -0,0 +1,160 @@
+---
+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`.
+
+To support IFD, the Evaluator also needs access to store paths. This
+could be implemented by having the coordinator provide an interface to retrieve
+files from a store path, or by ensuring a "realized version of the store" is
+accessible by the evaluator (this could be a FUSE filesystem, or the "real"
+/nix/store on disk.
+
+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.
+
+The environment in which builds happen is currently very Nix-specific. We might
+want to avoid having to maintain all the intricacies of a Nix-specific
+sandboxing environment in every builder, and instead only provide a more
+generic interface, receiving build requests (and have the coordinator translate
+derivations to that format). [^1]
+
+To build, the builder needs to be able to mount all build inputs into the build
+environment. For this, it needs the store to expose a filesystem interface.
+
+## Store
+
+*Purpose:* Store takes care of storing build results. It provides a
+unified interface to get store paths and upload new ones, as well as querying
+for the existence of a store path and its metadata (references, signatures, …).
+
+Tvix natively uses an improved store protocol. Instead of transferring around
+NAR files, which don't provide an index and don't allow seekable access, a
+concept similar to git tree hashing is used.
+
+This allows more granular substitution, chunk reusage and parallel download of
+individual files, reducing bandwidth usage.
+As these chunks are content-addressed, it opens up the potential for
+peer-to-peer trustless substitution of most of the data, as long as we sign the
+root of the index.
+
+Tvix still keeps the old-style signatures, NAR hashes and NAR size around. In
+the case of NAR hash / NAR size, this data is strictly required in some cases.
+The old-style signatures are valuable for communication with existing
+implementations.
+
+Old-style binary caches (like cache.nixos.org) can still be exposed via the new
+interface, by doing on-the-fly (re)chunking/ingestion.
+
+Most likely, there will be multiple implementations of store, some storing
+things locally, some exposing a "remote view".
+
+A few possible ones that come to mind are:
+
+- Local store
+- SFTP/ GCP / S3 / HTTP
+- NAR/NARInfo protocol: HTTP, S3
+
+A remote Tvix store can be connected by simply connecting to its gRPC
+interface, possibly using SSH tunneling, but there doesn't need to be an
+additional "wire format" like the Nix `ssh(+ng)://` protocol.
+
+Settling on one interface allows composition of stores, meaning it becomes
+possible to express substitution from remote caches as a proxy layer.
+
+It'd also be possible to write a FUSE implementation on top of the RPC
+interface, exposing a lazily-substituting /nix/store mountpoint. Using this in
+remote build context dramatically reduces the amount of data transferred to a
+builder, as only the files really accessed during the build are substituted.
+
+# Figures
+
+![component flow](./component-flow.svg)
+
+[^1]: There have already been some discussions in the Nix community, to switch
+  to REAPI:
+  https://discourse.nixos.org/t/a-proposal-for-replacing-the-nix-worker-protocol/20926/22
diff --git a/tvix/docs/default.nix b/tvix/docs/default.nix
new file mode 100644
index 000000000000..016d641df59f
--- /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/lang-version.md b/tvix/docs/lang-version.md
new file mode 100644
index 000000000000..c09422a5897a
--- /dev/null
+++ b/tvix/docs/lang-version.md
@@ -0,0 +1,60 @@
+# Nix language version history
+
+The Nix language (“Nix”) has its own versioning mechanism independent from its
+most popular implementation (“C++ Nix”): `builtins.langVersion`. It has been
+increased whenever the language has changed syntactically or semantically in a
+way that would not be introspectable otherwise. In particular, this does not
+include addition (or removal) of `builtins`, as this can be introspected using
+standard attribute set operations.
+
+Changes to `builtins.langVersion` are best found by viewing the git history of
+C++ Nix using `git log -G 'mkInt\\(v, [0-9]\\)'` for `builtins.langVersion` < 7.
+After that point `git log -G 'v\\.mkInt\\([0-9]+\\)'` should work. To reduce the
+amount of false positives, specify the version number you are interested in
+explicitly.
+
+## 1
+
+The first version of the Nix language is its state at the point when
+`builtins.langVersion` was added in [8b8ee53] which was first released
+as part of C++ Nix 1.2.
+
+## 2
+
+Nix version 2 changed the behavior of `builtins.storePath`: It would now [try to
+substitute the given path if missing][storePath-substitute], instead of creating
+an evaluation failure. `builtins.langVersion` was increased in [e36229d].
+
+## 3
+
+Nix version 3 changed the behavior of the `==` behavior. Strings would now be
+considered [equal even if they had differing string context][equal-no-ctx].
+
+## 4
+
+Nix version 4 [added the float type][float] to the language.
+
+## 5
+
+The [increase of `builtins.langVersion` to 5][langVersion-5] did not signify a
+language change, but added support for structured attributes to the Nix daemon.
+Eelco Dolstra writes as to what changed:
+
+> The structured attributes support. Unfortunately that's not so much a language
+> change as a build.cc (i.e. daemon) change, but we don't really have a way to
+> express that...
+
+Probably `builtins.nixVersion` (which was added in version 1) should have been
+used instead.
+
+## 6
+
+Nix version 6 added support for [comparing two lists][list-comparison].
+
+[8b8ee53]: https://github.com/nixos/nix/commit/8b8ee53bc73769bb25d967ba259dabc9b23e2e6f
+[storePath-substitute]: https://github.com/nixos/nix/commit/22d665019a3770148929b7504c73bcdbe025ec12
+[e36229d]: https://github.com/nixos/nix/commit/e36229d27f9ab508e0abf1892f3e8c263d2f8c58
+[equal-no-ctx]: https://github.com/nixos/nix/commit/ee7fe64c0ac00f2be11604a2a6509eb86dc19f0a
+[float]: https://github.com/nixos/nix/commit/14ebde52893263930cdcde1406cc91cc5c42556f
+[langVersion-5]: https://github.com/nixos/nix/commit/8191992c83bf4387b03c5fdaba818dc2b520462d
+[list-comparison]: https://github.com/nixos/nix/commit/09471d2680292af48b2788108de56a8da755d661
diff --git a/tvix/docs/language-spec.md b/tvix/docs/language-spec.md
new file mode 100644
index 000000000000..a71437493307
--- /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/docs/value-pointer-equality.md b/tvix/docs/value-pointer-equality.md
new file mode 100644
index 000000000000..78b1466f3a8f
--- /dev/null
+++ b/tvix/docs/value-pointer-equality.md
@@ -0,0 +1,202 @@
+# Value Pointer Equality in Nix
+
+## Introduction
+
+It is a piece of semi-obscure Nix trivia that while functions are generally not
+comparable, they can be compared in certain situations. This is actually quite an
+important fact, as it is essential for the evaluation of nixpkgs: The attribute sets
+used to represent platforms in nixpkgs, like `stdenv.buildPlatform`, contain functions,
+such as `stdenv.buildPlatform.canExecute`. When writing cross logic, one invariably
+ends up writing expressions that compare these sets, e.g. `stdenv.buildPlatform !=
+stdenv.hostPlatform`. Since attribute set equality is the equality of their attribute
+names and values, we also end up comparing the functions within them.  We can summarize
+the relevant part of this behavior for platform comparisons in the following (true)
+Nix expressions:
+
+* `stdenv.hostPlatform.canExecute != stdenv.hostPlatform.canExecute`
+* `stdenv.hostPlatform == stdenv.hostPlatform`
+
+This fact is commonly referred to as pointer equality of functions (or function pointer
+equality) which is not an entirely accurate name, as we'll see. This account of the
+behavior states that, while functions are incomparable in general, they are comparable
+insofar, as they occupy the same spot in an attribute set.
+
+However, [a maybe lesser known trick][puck-issue] is to write a function such as the
+following to allow comparing functions:
+
+```nix
+let
+  pointerEqual = lhs: rhs: { x = lhs; } == { x = rhs; };
+
+  f = name: "Hello, my name is ${name}";
+  g = name: "Hello, my name is ${name}";
+in
+[
+  (pointerEqual f f) # => true
+  (pointerEqual f g) # => false
+]
+```
+
+Here, clearly, the function is not contained at the same position in one and the same
+attribute set, but at the same position in two entirely different attribute sets. We can
+also see that we are not comparing the functions themselves (e.g. their AST), but
+rather if they are the same individual value (i.e. pointer equal).
+
+So what is _actually_ going on?
+
+## Nix (pointer) Equality in C++ Nix
+
+TIP: The summary presented here is up-to-date as of 2022-11-23 and was tested with Nix 2.3 and 2.11.
+
+The function implementing equality in C++ Nix is `EvalState::eqValues` which starts with
+[the following bit of code][eqValues-pointer-eq]:
+
+```cpp
+bool EvalState::eqValues(Value & v1, Value & v2)
+{
+    forceValue(v1);
+    forceValue(v2);
+
+    /* !!! Hack to support some old broken code that relies on pointer
+       equality tests between sets.  (Specifically, builderDefs calls
+       uniqList on a list of sets.)  Will remove this eventually. */
+    if (&v1 == &v2) return true;
+```
+
+So this immediately looks more like pointer equality of arbitrary *values* instead of functions. In fact
+there is [no special code facilitating function equality][eqValues-function-eq]:
+
+```cpp
+        /* Functions are incomparable. */
+        case nFunction:
+            return false;
+```
+
+So one takeaway of this is that pointer equality is neither dependent on functions nor attribute sets.
+In fact, we can also write our `pointerEqual` function as:
+
+```nix
+lhs: rhs: [ lhs ] == [ rhs ]
+```
+
+It's interesting that `EvalState::eqValues` forces the left and right-hand value before trying pointer
+equality. It explains that `let x = throw ""; in x == x` does not evaluate successfully, but it is puzzling why
+`let f = x: x; in f == f` does not return `true`. In fact, why do we need to wrap the values in a list or
+attribute set at all for our `pointerEqual` function to work?
+
+The answer lies in [the code that evaluates `ExprOpEq`][ExprOpEq],
+i.e. an expression involving the `==` operator:
+
+```cpp
+void ExprOpEq::eval(EvalState & state, Env & env, Value & v)
+{
+    Value v1; e1->eval(state, env, v1);
+    Value v2; e2->eval(state, env, v2);
+    v.mkBool(state.eqValues(v1, v2));
+}
+```
+
+As you can see, two _distinct_ `Value` structs are created, so they can never be pointer equal even
+if the `union` inside points to the same bit of memory. We can thus understand what actually happens
+when we check the equality of an attribute set (or list), by looking at the following expression:
+
+```nix
+let
+  x = { name = throw "nameless"; };
+in
+
+x == x # => causes an evaluation error
+```
+
+Because `x` can't be pointer equal, as it'll end up in the distinct structs `v1` and `v2`, it needs to be compared
+by value. For this reason, the `name` attribute will be forced and an evaluation error caused.
+If we rewrite the expression to use…
+
+```nix
+{ inherit x; } == { inherit x; } # => true
+```
+
+…, it'll work: The two attribute sets are compared by value, but their `x` attribute turns out to be pointer
+equal _after_ forcing it. This does not throw, since forcing an attribute set does not force its attributes'
+values (as forcing a list doesn't force its elements).
+
+As we have seen, pointer equality can not only be used to compare function values, but also other
+otherwise incomparable values, such as lists and attribute sets that would cause an evaluation
+error if they were forced recursively. We can even switch out the `throw` for an `abort`. The limitation is
+of course that we need to use a value that behaves differently depending on whether it is forced
+“normally” (think `builtins.seq`) or recursively (think `builtins.deepSeq`), so thunks will generally be
+evaluated before pointer equality can kick into effect.
+
+## Summary
+
+When comparing two Nix values, we must force both of them (non-recursively!), but are
+allowed to short-circuit the comparison based on pointer equality, i.e. if they are at
+the same exact value in memory, they are deemed equal immediately. This is completely
+independent of what type of value they are. If they are not pointer equal, they are
+(recursively) compared by value as expected.
+
+However, when evaluating the Nix expression `a == b`, we *must* invoke our implementation's
+value equality function in a way that `a` and `b` themselves can never be deemed pointer equal.
+Any values we encounter while recursing during the equality check must be compared by
+pointer as described above, though.
+
+## Other Comparisons
+
+The `!=` operator uses `EvalState::eqValues` internally as well, so it behaves exactly as `!(a == b)`.
+
+The `>`, `<`, `>=` and `<=` operators all desugar to [CompareValues][] eventually
+which generally looks at the value type before comparing. It does, however, rely on
+`EvalState::eqValues` for list comparisons, so it is possible to compare lists with
+e.g. functions in them, as long as they are equal by pointer:
+
+```nix
+let
+  f = x: x + 42;
+in
+
+[
+  ([ f 2 ] > [ f 1 ]) # => true
+  ([ f 2 ] > [ (x: x) 1]) # => error: cannot compare a function with a function
+]
+```
+
+Finally, since `builtins.elem` relies on `EvalState::eqValues`, you can check for
+a function by pointer equality:
+
+```nix
+let
+  f = x: f x;
+in
+builtins.elem f [ f 2 3 ] # => true
+```
+
+## Stability of the Feature
+
+Keen readers will have noticed the following comment in the C++ Nix source code,
+indicating that pointer comparison may be removed in the future.
+
+```cpp
+    /* !!! Hack to support some old broken code that relies on pointer
+       equality tests between sets.  (Specifically, builderDefs calls
+       uniqList on a list of sets.)  Will remove this eventually. */
+```
+
+Now, I can't speak for the upstream C++ Nix developers, but sure can speculate.
+As already pointed out, this feature is currently needed for evaluating nixpkgs.
+While its use could realistically be eliminated (only bothersome spot is probably
+the `emulator` function, but that should also be doable), removing the feature
+would seriously compromise C++ Nix's ability to evaluate historical nixpkgs
+revision which is arguably a strength of the system.
+
+Another indication that it is likely here to stay is that it has already
+[outlived builderDefs][], even though
+it was (apparently) reintroduced just for this use case. More research into
+the history of this feature would still be prudent, especially the reason for
+its original introduction (maybe performance?).
+
+[puck-issue]: https://github.com/NixOS/nix/issues/3371
+[eqValues-pointer-eq]: https://github.com/NixOS/nix/blob/05d0892443bbe92a6b6a1ee7b1d37ea05782d918/src/libexpr/eval.cc#L2342-L2350
+[eqValues-function-eq]: https://github.com/NixOS/nix/blob/05d0892443bbe92a6b6a1ee7b1d37ea05782d918/src/libexpr/eval.cc#L2405-L2407
+[ExprOpEq]: https://github.com/NixOS/nix/blob/05d0892443bbe92a6b6a1ee7b1d37ea05782d918/src/libexpr/eval.cc#L1856-L1861
+[outlived builderDefs]: https://github.com/NixOS/nixpkgs/issues/4210
+[CompareValues]: https://github.com/NixOS/nix/blob/master/src/libexpr/primops.cc#L536-L574
diff --git a/tvix/eval/.skip-subtree b/tvix/eval/.skip-subtree
new file mode 100644
index 000000000000..05f9fc116f11
--- /dev/null
+++ b/tvix/eval/.skip-subtree
@@ -0,0 +1,2 @@
+Do not traverse further, readTree will encounter Nix language tests
+and such and fail.
diff --git a/tvix/eval/Cargo.toml b/tvix/eval/Cargo.toml
new file mode 100644
index 000000000000..99c4fc1a1106
--- /dev/null
+++ b/tvix/eval/Cargo.toml
@@ -0,0 +1,60 @@
+[package]
+name = "tvix-eval"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+name = "tvix_eval"
+
+[dependencies]
+builtin-macros = { path = "./builtin-macros", package = "tvix-eval-builtin-macros" }
+codemap = "0.1.3"
+codemap-diagnostic = "0.1.1"
+dirs = "4.0.0"
+genawaiter = { version = "0.99.1", default_features = false }
+imbl = { version = "2.0", features = [ "serde" ] }
+lazy_static = "1.4.0"
+lexical-core = { version = "0.8.5", features = ["format", "parse-floats"] }
+path-clean = "0.1"
+proptest = { version = "1.0.0", default_features = false, features = ["std", "alloc", "break-dead-code", "tempfile"], optional = true }
+regex = "1.6.0"
+rnix = "0.11.0"
+rowan = "*" # pinned by rnix
+serde = { version = "1.0", features = [ "rc", "derive" ] }
+serde_json = "1.0"
+smol_str = "0.2.0"
+tabwriter = "1.2"
+test-strategy = { version = "0.2.1", optional = true }
+toml = "0.6.0"
+xml-rs = "0.8.4"
+
+[dev-dependencies]
+criterion = "0.4"
+pretty_assertions = "1.2.1"
+itertools = "0.10.3"
+tempdir = "0.3.7"
+
+[dev-dependencies.test-generator]
+# This fork of test-generator adds support for cargo workspaces, see
+# also https://github.com/frehberg/test-generator/pull/14
+git = "https://github.com/JamesGuthrie/test-generator.git"
+rev = "82e799979980962aec1aa324ec6e0e4cad781f41"
+
+[features]
+default = ["impure", "arbitrary", "nix_tests"]
+
+# Enables running the Nix language test suite from the original C++
+# Nix implementation (at version 2.3) against Tvix.
+nix_tests = []
+
+# Enables operations in the VM which depend on the ability to perform I/O
+impure = []
+
+# Enables Arbitrary impls for internal types (required to run tests)
+arbitrary = ["proptest", "test-strategy", "imbl/proptest"]
+
+[[bench]]
+name = "eval"
+harness = false
diff --git a/tvix/eval/README.md b/tvix/eval/README.md
new file mode 100644
index 000000000000..04bc6c3eb82b
--- /dev/null
+++ b/tvix/eval/README.md
@@ -0,0 +1,58 @@
+Tvix Evaluator
+==============
+
+This project implements an interpreter for the Nix programming
+language. You can experiment with an online version of the evaluator:
+[tvixbolt][].
+
+The interpreter aims to be compatible with `nixpkgs`, on the
+foundation of Nix 2.3.
+
+**Important note:** The evaluator is not yet feature-complete, and
+while the core mechanisms (compiler, runtime, ...) have stabilised
+somewhat, a lot of components are still changing rapidly.
+
+Please contact [TVL](https://tvl.fyi) with any questions you might
+have.
+
+## Building tvix-eval
+
+Please check the `README.md` one level up for instructions on how to build this.
+
+The evaluator itself can also be built with standard Rust tooling (i.e. `cargo
+build`).
+
+If you would like to clone **only** the evaluator and build it
+directly with Rust tooling, you can do:
+
+```bash
+git clone https://code.tvl.fyi/depot.git:/tvix/eval.git tvix-eval
+
+cd tvix-eval && cargo build
+```
+
+## Nix test suite
+
+C++ Nix implements a language test suite in the form of Nix source
+code files, and their expected output. The coverage of this test suite
+is not complete, but we intend to be compatible with it.
+
+We have ported the test suite to Tvix, but do not run it by default as
+we are not yet compatible with it.
+
+You can run the test suite by enabling the `nix_tests` feature in
+Cargo:
+
+    cargo test --features nix_tests
+
+## rnix-parser
+
+Tvix is written in memory of jD91mZM2, the author of [rnix-parser][]
+who sadly [passed away][rip].
+
+Tvix makes heavy use of rnix-parser in its bytecode compiler. The
+parser is now maintained by Nix community members.
+
+[rnix-parser]: https://github.com/nix-community/rnix-parser
+[rip]: https://www.redox-os.org/news/open-source-mental-health/
+[tvixbolt]: https://tazj.in/blobs/tvixbolt/
diff --git a/tvix/eval/benches/eval.rs b/tvix/eval/benches/eval.rs
new file mode 100644
index 000000000000..f85e3585f017
--- /dev/null
+++ b/tvix/eval/benches/eval.rs
@@ -0,0 +1,39 @@
+use criterion::{black_box, criterion_group, criterion_main, Criterion};
+use itertools::Itertools;
+
+fn interpret(code: &str) {
+    tvix_eval::Evaluation::new(code, None).evaluate();
+}
+
+fn eval_literals(c: &mut Criterion) {
+    c.bench_function("int", |b| {
+        b.iter(|| {
+            interpret("42");
+            black_box(())
+        })
+    });
+}
+
+fn eval_merge_attrs(c: &mut Criterion) {
+    c.bench_function("merge small attrs", |b| {
+        b.iter(|| {
+            interpret("{ a = 1; b = 2; } // { c = 3; }");
+            black_box(())
+        })
+    });
+
+    c.bench_function("merge large attrs with small attrs", |b| {
+        let large_attrs = format!(
+            "{{{}}}",
+            (0..10000).map(|n| format!("a{n} = {n};")).join(" ")
+        );
+        let expr = format!("{large_attrs} // {{ c = 3; }}");
+        b.iter(move || {
+            interpret(&expr);
+            black_box(())
+        })
+    });
+}
+
+criterion_group!(benches, eval_literals, eval_merge_attrs);
+criterion_main!(benches);
diff --git a/tvix/eval/build.rs b/tvix/eval/build.rs
new file mode 100644
index 000000000000..a9c9a78b060d
--- /dev/null
+++ b/tvix/eval/build.rs
@@ -0,0 +1,9 @@
+use std::env;
+
+fn main() {
+    println!(
+        "cargo:rustc-env=TVIX_CURRENT_SYSTEM={}",
+        &env::var("TARGET").unwrap()
+    );
+    println!("cargo:rerun-if-changed-env=TARGET")
+}
diff --git a/tvix/eval/builtin-macros/.gitignore b/tvix/eval/builtin-macros/.gitignore
new file mode 100644
index 000000000000..eb5a316cbd19
--- /dev/null
+++ b/tvix/eval/builtin-macros/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/tvix/eval/builtin-macros/Cargo.toml b/tvix/eval/builtin-macros/Cargo.toml
new file mode 100644
index 000000000000..3a35ea12a0c0
--- /dev/null
+++ b/tvix/eval/builtin-macros/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "tvix-eval-builtin-macros"
+version = "0.0.1"
+authors = [ "Griffin Smith <root@gws.fyi>" ]
+edition = "2021"
+
+[dependencies]
+syn = { version = "1.0.57", features = ["full", "parsing", "printing", "visit", "visit-mut", "extra-traits"] }
+quote = "1.0.8"
+proc-macro2 = "1"
+
+[lib]
+proc-macro = true
+
+[dev-dependencies]
+tvix-eval = { path = "../" }
diff --git a/tvix/eval/builtin-macros/src/lib.rs b/tvix/eval/builtin-macros/src/lib.rs
new file mode 100644
index 000000000000..dfd0948c7d9f
--- /dev/null
+++ b/tvix/eval/builtin-macros/src/lib.rs
@@ -0,0 +1,334 @@
+extern crate proc_macro;
+
+use proc_macro::TokenStream;
+use proc_macro2::Span;
+use quote::{quote, quote_spanned, ToTokens};
+use syn::parse::Parse;
+use syn::spanned::Spanned;
+use syn::{
+    parse2, parse_macro_input, parse_quote, parse_quote_spanned, Attribute, FnArg, Ident, Item,
+    ItemMod, LitStr, Meta, Pat, PatIdent, PatType, Token, Type,
+};
+
+/// Description of a single argument passed to a builtin
+struct BuiltinArgument {
+    /// The name of the argument, to be used in docstrings and error messages
+    name: Ident,
+
+    /// Type of the argument.
+    ty: Box<Type>,
+
+    /// Whether the argument should be forced before the underlying builtin
+    /// function is called.
+    strict: bool,
+
+    /// Span at which the argument was defined.
+    span: Span,
+}
+
+fn extract_docstring(attrs: &[Attribute]) -> Option<String> {
+    // Rust docstrings are transparently written pre-macro expansion into an attribute that looks
+    // like:
+    //
+    // #[doc = "docstring here"]
+    //
+    // Multi-line docstrings yield multiple attributes in order, which we assemble into a single
+    // string below.
+
+    #[allow(dead_code)]
+    #[derive(Debug)]
+    struct Docstring {
+        eq: Token![=],
+        doc: LitStr,
+    }
+
+    impl Parse for Docstring {
+        fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+            Ok(Self {
+                eq: input.parse()?,
+                doc: input.parse()?,
+            })
+        }
+    }
+
+    attrs
+        .iter()
+        .filter(|attr| attr.path.get_ident().into_iter().any(|id| id == "doc"))
+        .filter_map(|attr| parse2::<Docstring>(attr.tokens.clone()).ok())
+        .map(|docstring| docstring.doc.value())
+        .reduce(|mut fst, snd| {
+            if snd.is_empty() {
+                // An empty string represents a spacing newline that was added in the
+                // original doc comment.
+                fst.push_str("\n\n");
+            } else {
+                fst.push_str(&snd);
+            }
+
+            fst
+        })
+}
+
+/// Parse arguments to the `builtins` macro itself, such as `#[builtins(state = Rc<State>)]`.
+fn parse_module_args(args: TokenStream) -> Option<Type> {
+    if args.is_empty() {
+        return None;
+    }
+
+    let meta: Meta = syn::parse(args).expect("could not parse arguments to `builtins`-attribute");
+    let name_value = match meta {
+        Meta::NameValue(nv) => nv,
+        _ => panic!("arguments to `builtins`-attribute must be of the form `name = value`"),
+    };
+
+    if name_value.path.get_ident().unwrap().to_string() != "state" {
+        return None;
+    }
+
+    if let syn::Lit::Str(type_name) = name_value.lit {
+        let state_type: Type =
+            syn::parse_str(&type_name.value()).expect("failed to parse builtins state type");
+        return Some(state_type);
+    }
+
+    panic!("state attribute must be a quoted Rust type");
+}
+
+/// Mark the annotated module as a module for defining Nix builtins.
+///
+/// An optional type definition may be specified as an argument (e.g. `#[builtins(Rc<State>)]`),
+/// which will add a parameter to the `builtins` function of that type which is passed to each
+/// builtin upon instantiation. Using this, builtins that close over some external state can be
+/// written.
+///
+/// The type of each function is rewritten to receive a `Vec<Value>`, containing each `Value`
+/// argument that the function receives. The body of functions is accordingly rewritten to "unwrap"
+/// values from this vector and bind them to the correct names, so unless a static error occurs this
+/// transformation is mostly invisible to users of the macro.
+///
+/// A function `fn builtins() -> Vec<Builtin>` will be defined within the annotated module,
+/// returning a list of [`tvix_eval::Builtin`] for each function annotated with the `#[builtin]`
+/// attribute within the module. If a `state` type is specified, the `builtins` function will take a
+/// value of that type.
+///
+/// Each invocation of the `#[builtin]` annotation within the module should be passed a string
+/// literal for the name of the builtin.
+///
+/// # Examples
+/// ```ignore
+/// # use tvix_eval_builtin_macros::builtins;
+/// # mod value {
+/// #     pub use tvix_eval::Builtin;
+/// # }
+///
+/// #[builtins]
+/// mod builtins {
+///     use tvix_eval::{GenCo, ErrorKind, Value};
+///
+///     #[builtin("identity")]
+///     pub async fn builtin_identity(co: GenCo, x: Value) -> Result<Value, ErrorKind> {
+///         Ok(x)
+///     }
+///
+///     // Builtins can request their argument not be forced before being called by annotating the
+///     // argument with the `#[lazy]` attribute
+///
+///     #[builtin("tryEval")]
+///     pub async fn builtin_try_eval(co: GenCo, #[lazy] x: Value) -> Result<Value, ErrorKind> {
+///         todo!()
+///     }
+/// }
+/// ```
+#[proc_macro_attribute]
+pub fn builtins(args: TokenStream, item: TokenStream) -> TokenStream {
+    let mut module = parse_macro_input!(item as ItemMod);
+
+    // parse the optional state type, which users might want to pass to builtins
+    let state_type = parse_module_args(args);
+
+    let (_, items) = match &mut module.content {
+        Some(content) => content,
+        None => {
+            return (quote_spanned!(module.span() =>
+                compile_error!("Builtin modules must be defined in-line")
+            ))
+            .into();
+        }
+    };
+
+    let mut builtins = vec![];
+    for item in items.iter_mut() {
+        if let Item::Fn(f) = item {
+            if let Some(builtin_attr_pos) = f
+                .attrs
+                .iter()
+                .position(|attr| attr.path.get_ident().iter().any(|id| *id == "builtin"))
+            {
+                let builtin_attr = f.attrs.remove(builtin_attr_pos);
+                let name: LitStr = match builtin_attr.parse_args() {
+                    Ok(args) => args,
+                    Err(err) => return err.into_compile_error().into(),
+                };
+
+                if f.sig.inputs.len() <= 1 {
+                    return (quote_spanned!(
+                        f.sig.inputs.span() =>
+                            compile_error!("Builtin functions must take at least two arguments")
+                    ))
+                    .into();
+                }
+
+                // Inspect the first argument to determine if this function is
+                // taking the state parameter.
+                // TODO(tazjin): add a test in //tvix/eval that covers this
+                let mut captures_state = false;
+                if let FnArg::Typed(PatType { pat, .. }) = &f.sig.inputs[0] {
+                    if let Pat::Ident(PatIdent { ident, .. }) = pat.as_ref() {
+                        if ident.to_string() == "state" {
+                            if state_type.is_none() {
+                                panic!("builtin captures a `state` argument, but no state type was defined");
+                            }
+
+                            captures_state = true;
+                        }
+                    }
+                }
+
+                let mut rewritten_args = std::mem::take(&mut f.sig.inputs)
+                    .into_iter()
+                    .collect::<Vec<_>>();
+
+                // Split out the value arguments from the static arguments.
+                let split_idx = if captures_state { 2 } else { 1 };
+                let value_args = rewritten_args.split_off(split_idx);
+
+                let builtin_arguments = value_args
+                    .into_iter()
+                    .map(|arg| {
+                        let span = arg.span();
+                        let mut strict = true;
+                        let (name, ty) = match arg {
+                            FnArg::Receiver(_) => {
+                                return Err(quote_spanned!(span => {
+                                    compile_error!("unexpected receiver argument in builtin")
+                                }))
+                            }
+                            FnArg::Typed(PatType {
+                                mut attrs, pat, ty, ..
+                            }) => {
+                                attrs.retain(|attr| {
+                                    attr.path.get_ident().into_iter().any(|id| {
+                                        if id == "lazy" {
+                                            strict = false;
+                                            false
+                                        } else {
+                                            true
+                                        }
+                                    })
+                                });
+                                match pat.as_ref() {
+                                    Pat::Ident(PatIdent { ident, .. }) => {
+                                        (ident.clone(), ty.clone())
+                                    }
+                                    _ => panic!("ignored value parameters must be named, e.g. `_x` and not just `_`"),
+                                }
+                            }
+                        };
+
+                        Ok(BuiltinArgument {
+                            strict,
+                            span,
+                            name,
+                            ty,
+                        })
+                    })
+                    .collect::<Result<Vec<BuiltinArgument>, _>>();
+
+                let builtin_arguments = match builtin_arguments {
+                    Err(err) => return err.into(),
+
+                    // reverse argument order, as they are popped from the stack
+                    // slice in opposite order
+                    Ok(args) => args,
+                };
+
+                // Rewrite the argument to the actual function to take a
+                // `Vec<Value>`, which is then destructured into the
+                // user-defined values in the function header.
+                let sig_span = f.sig.span();
+                rewritten_args.push(parse_quote_spanned!(sig_span=> mut values: Vec<Value>));
+                f.sig.inputs = rewritten_args.into_iter().collect();
+
+                // Rewrite the body of the function to do said argument forcing.
+                //
+                // This is done by creating a new block for each of the
+                // arguments that evaluates it, and wraps the inner block.
+                for arg in &builtin_arguments {
+                    let block = &f.block;
+                    let ty = &arg.ty;
+                    let ident = &arg.name;
+
+                    if arg.strict {
+                        f.block = Box::new(parse_quote_spanned! {arg.span=> {
+                            let #ident: #ty = generators::request_force(&co, values.pop()
+                              .expect("Tvix bug: builtin called with incorrect number of arguments")).await;
+
+                            #block
+                        }});
+                    } else {
+                        f.block = Box::new(parse_quote_spanned! {arg.span=> {
+                            let #ident: #ty = values.pop()
+                              .expect("Tvix bug: builtin called with incorrect number of arguments");
+
+                            #block
+                        }})
+                    }
+                }
+
+                let fn_name = f.sig.ident.clone();
+                let arg_count = builtin_arguments.len();
+                let docstring = match extract_docstring(&f.attrs) {
+                    Some(docs) => quote!(Some(#docs)),
+                    None => quote!(None),
+                };
+
+                if captures_state {
+                    builtins.push(quote_spanned! { builtin_attr.span() => {
+                        let inner_state = state.clone();
+                        crate::Builtin::new(
+                            #name,
+                            #docstring,
+                            #arg_count,
+                            move |values| Gen::new(|co| generators::pin_generator(#fn_name(inner_state.clone(), co, values))),
+                        )
+                    }});
+                } else {
+                    builtins.push(quote_spanned! { builtin_attr.span() => {
+                        crate::Builtin::new(
+                            #name,
+                            #docstring,
+                            #arg_count,
+                            |values| Gen::new(|co| generators::pin_generator(#fn_name(co, values))),
+                        )
+                    }});
+                }
+            }
+        }
+    }
+
+    if let Some(state_type) = state_type {
+        items.push(parse_quote! {
+            pub fn builtins(state: #state_type) -> Vec<(&'static str, Value)> {
+                vec![#(#builtins),*].into_iter().map(|b| (b.name(), Value::Builtin(b))).collect()
+            }
+        });
+    } else {
+        items.push(parse_quote! {
+            pub fn builtins() -> Vec<(&'static str, Value)> {
+                vec![#(#builtins),*].into_iter().map(|b| (b.name(), Value::Builtin(b))).collect()
+            }
+        });
+    }
+
+    module.into_token_stream().into()
+}
diff --git a/tvix/eval/builtin-macros/tests/tests.rs b/tvix/eval/builtin-macros/tests/tests.rs
new file mode 100644
index 000000000000..735ff4672007
--- /dev/null
+++ b/tvix/eval/builtin-macros/tests/tests.rs
@@ -0,0 +1,45 @@
+pub use tvix_eval::{Builtin, Value};
+use tvix_eval_builtin_macros::builtins;
+
+#[builtins]
+mod builtins {
+    use tvix_eval::generators::{self, Gen, GenCo};
+    use tvix_eval::{ErrorKind, Value};
+
+    /// Test docstring.
+    ///
+    /// It has multiple lines!
+    #[builtin("identity")]
+    pub async fn builtin_identity(co: GenCo, x: Value) -> Result<Value, ErrorKind> {
+        Ok(todo!())
+    }
+
+    #[builtin("tryEval")]
+    pub async fn builtin_try_eval(co: GenCo, #[lazy] _x: Value) -> Result<Value, ErrorKind> {
+        todo!()
+    }
+}
+
+#[test]
+fn builtins() {
+    let builtins = builtins::builtins();
+    assert_eq!(builtins.len(), 2);
+
+    let (_, identity) = builtins
+        .iter()
+        .find(|(name, _)| *name == "identity")
+        .unwrap();
+
+    match identity {
+        Value::Builtin(identity) => assert_eq!(
+            identity.documentation(),
+            Some(
+                r#" Test docstring.
+
+ It has multiple lines!"#
+            )
+        ),
+
+        _ => panic!("builtin was not a builtin"),
+    }
+}
diff --git a/tvix/eval/default.nix b/tvix/eval/default.nix
new file mode 100644
index 000000000000..91661291f7b6
--- /dev/null
+++ b/tvix/eval/default.nix
@@ -0,0 +1,9 @@
+# TODO: find a way to build the benchmarks via crate2nix
+{ depot, pkgs, ... }:
+
+depot.tvix.crates.workspaceMembers.tvix-eval.build.override {
+  runTests = true;
+
+  # Make C++ Nix available, to compare eval results against.
+  testInputs = [ pkgs.nix ];
+}
diff --git a/tvix/eval/docs/abandoned/thread-local-vm.md b/tvix/eval/docs/abandoned/thread-local-vm.md
new file mode 100644
index 000000000000..c6a2d5e07e5c
--- /dev/null
+++ b/tvix/eval/docs/abandoned/thread-local-vm.md
@@ -0,0 +1,233 @@
+# We can't have nice things because IFD
+
+The thread-local VM work below was ultimately not merged because it
+was decided that it would be harmful for `tvix::eval::Value` to
+implement `Eq`, `Hash`, or any of the other `std` traits.
+
+Implementing `std` traits on `Value` was deemed harmful because IFD
+can cause arbitrary amounts of compilation to occur, including
+network transactions with builders.  Obviously it would be
+unexpected and error-prone to have a `PartialEq::eq()` which does
+something like this.  This problem does not manifest within the
+"nixpkgs compatibility only" scope, or in any undeprecated language
+feature other than IFD.  Although IFD is outside the "nixpkgs
+compatibility scope", it [has been added to the TVL compatibility
+scope](https://cl.tvl.fyi/c/depot/+/7193/comment/3418997b_0dbd0b65/).
+
+This was the sole reason for not merging.
+
+The explanation below may be useful in case future circumstances
+affect the relevance of the reasoning above.
+
+The implementation can be found in these CLs:
+
+- [refactor(tvix/eval): remove lifetime parameter from VM<'o>](https://cl.tvl.fyi/c/depot/+/7194)
+- [feat(tvix/eval): [FOUNDLING] thread-local VM](https://cl.tvl.fyi/c/depot/+/7195)
+- [feat(tvix/eval): [FOUNDLING] VM::vm_xxx convenience methods](https://cl.tvl.fyi/c/depot/+/7196)
+- [refactor(tvix/eval): [FOUNDLING]: drop explicit `&mut vm` parameter](https://cl.tvl.fyi/c/depot/+/7197)
+
+# Thread-local storage for tvix::eval::vm::VM
+
+## The problem
+
+`Value::force()` takes a `&mut VM` argument, since forcing a value
+requires executing opcodes.  This means that `Value::nix_eq()` too
+must take a `&mut VM`, since any sensible definition of equality
+will have to force thunks.
+
+Unfortunately Rust's `PartialEq::eq()` function does not accept any
+additional arguments like this, so `Value` cannot implement
+`PartialEq`.  Worse, structs which *contain* `Value`s can't
+implement `PartialEq` either.  This means `Value`, and anything
+containing it, cannot be the key for a `BTreeMap` or `HashMap`.  We
+can't even insert `Value`s into a `HashSet`!
+
+There are other situations like this that don't involve `PartialEq`,
+but it's the most glaring one.  The main problem is that you need a
+`VM` in order to force thunks, and thunks can be anywhere in a
+`Value`.
+
+## Solving the problem with thread-locals
+
+We could avoid threading the `&mut VM` through the entire codebase
+by making it a thread-local.
+
+To do this without a performance hit, we need to use LLVM
+thread-locals, which are the same cost as references to `static`s
+but load relative to
+[`llvm.threadlocal.address`][threadlocal-intrinsic] instead of
+relative to the data segment.  Unfortunately `#[thread_local]` [is
+unstable][thread-local-unstable] and [unsafe in
+general][thread-local-unsafe] for most of the cases where we would
+want to use it.  There is one [exception][tls-const-init], however:
+if a `!thread_local()` has a `const` initializer, the compiler will
+insert a `#[thread_local]`; this special case is both safe and
+stable.
+
+The difficult decision is what the type of the thread-local should
+be.  Since you can't get a mutable reference to a `thread_local!()`
+it will have to be some interior-mutability-bestowing wrapper around
+our current `struct VM`.  Here are the choices:
+
+### `RefCell<VM>`
+
+This is the obvious first choice, since it lets you borrow a
+`RefMut<Target=VM>`.  The problem here is that we want to keep the
+codebase written such that all the functions in `impl VM` still take
+a `&mut self`.  This means that there will be an active mutable
+borrow for the duration of `VM::call_builtin()`.  So if we implement
+`PartialEq` by having `eq()` attempt a second mutable borrow from
+the thread-local storage, it will fail since there is already an
+active borrow.
+
+The problem here is that you can't "unborrow" a `RefMut` except by
+dropping it.  There's no way around this.
+
+#### Problem: Uglification
+
+The only solution here is to rewrite all the functions in `impl VM`
+so they don't take any kind of `self` argument, and then have them
+do a short-lived `.borrow_mut()` from the thread-local `RefCell`
+*separately, each time* they want to modify one of the fields of
+`VM` (currently `frames`, `stack`, `with_stack`, `warnings`).  This
+means that if you had a code sequence like this:
+
+```
+impl VM {
+  fn foo(&mut self, ...) {
+    ...
+    self.frame().ip += 1;
+    self.some_other_method();
+    self.frame().ip += 1;
+```
+
+You would need to add *two separate `borrow_mut()`s*, one for each
+of the `self.frame().ip+=1` statements.  You can't just do one big
+`borrow_mut()` because `some_other_method()` will call
+`borrow_mut()` and panic.
+
+#### Problem: Performance
+
+The `RefCell<VM>` approach also has a fairly huge performance hit,
+because every single modification to any part of `VM` will require a
+reference count increment/decrement, and a conditional branch based
+on the check (which will never fail) that the `RefCell` isn't
+already mutably borrowed.  It will also impede a lot of rustc's
+optimizations.
+
+### `Cell<VM>`
+
+This is a non-starter because it means that in order to mutate any
+field of `VM`, you have to move the entire `struct VM` out of the
+`Cell`, mutate it, and move it back in.
+
+### `Cell<Box<VM>>`
+
+Now we're getting warmer.  Here, we can move the `Box<VM>` out of
+the cell with a single pointer-sized memory access.
+
+We don't want to do the "uglification" described in the previous
+section.  We are very fortunate that, sometime in mid-2019, the Rust
+dieties [decreed by fiat][fiat-decree] that `&Cell<T>` and `&mut T`
+are bit-for-bit identical, and even gave us mortals safe wrappers
+[`from_mut()`][from_mut] and [`get_mut()`][get_mut] around
+`mem::transmute()`.
+
+So now, when a `VM` method (which takes `&mut self`) calls out to
+some external code (like a builtin), instead of passing the `&mut
+self` to the external code it can call `Cell::from_mut(&mut self)`,
+and then `Cell::swap()` that into the thread-local storage cell for
+the duration of the external code.  After the external code returns,
+it can `Cell::swap()` it back.  This whole dance gets wrapped in a
+lexical block, and the borrow checker sees that the `&Cell<Box<VM>>`
+returned by `Cell::from_mut()` lives only until the end of the
+lexical block, *so we get the `&mut self` back after the close-brace
+for that block*.  NLL FTW.  This sounds like a lot of work, but it
+should compile down to two pointer-sized loads and two pointer-sized
+stores, and it is incurred basically only for `OpBuiltin`.
+
+This all works, with only two issues:
+
+1. `vm.rs` needs to be very careful to do the thread-local cell swap
+   dance before calling anything that might call `PartialEq::eq()`
+   (or any other method that expects to be able to pull the `VM` out
+   of thread-local storage).  There is no compile-time check that we
+   did the dance in all the right places.  If we forget to do the
+   dance somewhere we'll get a runtime panic from `Option::expect()`
+   (see next section).
+
+2. Since we need to call `Cell::from_mut()` on a `Box<VM>` rather
+   than a bare `VM`, we still need to rewrite all of `vm.rs` so that
+   every function takes a `&mut Box<VM>` instead of a `&mut self`.
+   This creates a huge amount of "noise" in the code.
+
+Fortunately, it turns out that nearly all the "noise" that arises
+from the second point can be eliminated by taking advantage of
+[deref coercions][deref-coercions]!  This was the last "shoe to
+drop".
+
+There is still the issue of having to be careful about calls from
+`vm.rs` to things outside that file, but it's manageable.
+
+### `Cell<Option<Box<VM>>>`
+
+In order to get the "safe and stable `#[thread_local]`"
+[exception][tls-const-init] we need a `const` initializer, which
+means we need to be able to put something into the `Cell` that isn't
+a `VM`.  So the type needs to be `Cell<Option<Box<VM>>>`.
+
+Recall that you can't turn an `Option<&T>` into an `&Option<T>`.
+The latter type has the "is this a `Some` or `None`" bit immediately
+adjacent to the bits representing `T`.  So if I hand you a `t:&T`
+and you wrap it as `Some(t)`, those bits aren't adjacent in memory.
+This means that all the VM methods need to operate on an
+`Option<Box<VM>>` -- we can't just wrap a `Some()` around `&mut
+self` "at the last minute" before inserting it into the thread-local
+storage cell.  Fortunately deref coercions save the day here too --
+the coercion is inferred through both layers (`Box` and `Option`) of
+wrapper, so there is no additional noise in the code.
+
+Note that Rust is clever and can find some sequence of bits that
+aren't a valid `T`, so `sizeof(Option<T>)==sizeof(T)`.  And in fact,
+`Box<T>` is one of these cases (and this is guaranteed).  So the
+`Option` has no overhead.
+
+# Closing thoughts, language-level support
+
+This would have been easier with language-level support.
+
+## What wouldn't help
+
+Although it [it was decreed][fiat-decree] that `Cell<T>` and `&mut
+T` are interchangeable, a `LocalKey<Cell<T>>` isn't quite the same
+thing as a `Cell<T>`, so it wouldn't be safe for the standard
+library to contain something like this:
+
+```
+impl<T> LocalKey<Cell<T>> {
+  fn get_mut(&self) -> &mut T {
+    unsafe {
+      // ... mem::transmute() voodoo goes here ...
+```
+
+The problem here is that you can call `LocalKey<Cell<T>>::get_mut()` twice and
+end up with two `&mut T`s that point to the same thing (mutable aliasing) which
+results in undefined behavior.
+
+## What would help
+
+The ideal solution is for Rust to let you call arbitrary methods
+`T::foo(&mut self...)` on a `LocalKey<Cell<T>>`.  This way you can
+have one (and only one) `&mut T` at any syntactical point in your
+program -- the `&mut self`.
+
+
+[tls-const-init]: https://github.com/rust-lang/rust/pull/90774
+[thread-local-unstable]: https://github.com/rust-lang/rust/issues/29594
+[thread-local-unsafe-generally]: https://github.com/rust-lang/rust/issues/54366
+[fiat-decree]: https://github.com/rust-lang/rust/issues/43038
+[from_mut]: https://doc.rust-lang.org/stable/std/cell/struct.Cell.html#method.from_mut
+[get_mut]: https://doc.rust-lang.org/stable/std/cell/struct.Cell.html#method.get_mut
+[thread-local-unsafe]: [https://github.com/rust-lang/rust/issues/54366]
+[deref-coercions]: https://doc.rust-lang.org/book/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods
+[threadlocal-intrinsic]: https://llvm.org/docs/LangRef.html#llvm-threadlocal-address-intrinsic
diff --git a/tvix/eval/docs/build-references.md b/tvix/eval/docs/build-references.md
new file mode 100644
index 000000000000..788b25c2be6c
--- /dev/null
+++ b/tvix/eval/docs/build-references.md
@@ -0,0 +1,175 @@
+Build references in derivations
+===============================
+
+This document describes how build references are calculated in Tvix. Build
+references are used to determine which store paths should be available to a
+builder during the execution of a build (i.e. the full build closure of a
+derivation).
+
+## String contexts in C++ Nix
+
+In C++ Nix, each string value in the evaluator carries an optional so-called
+"string context".
+
+These contexts are themselves a list of strings that take one of the following
+formats:
+
+1. `!<output_name>!<drv_path>`
+
+   This format describes a build reference to a specific output of a derivation.
+
+2. `=<drv_path>`
+
+   This format is used for a special case where a derivation attribute directly
+   refers to a derivation path (e.g. by accessing `.drvPath` on a derivation).
+
+   Note: In C++ Nix this case is quite special and actually requires a
+   store-database query during evaluation.
+
+3. `<path>` - a non-descript store path input, usually a plain source file (e.g.
+   from something like `src = ./.` or `src = ./foo.txt`).
+
+   In the case of `unsafeDiscardOutputDependency` this is used to pass a raw
+   derivation file, but *not* pull in its outputs.
+
+Lets introduce names for these (in the same order) to make them easier to
+reference below:
+
+```rust
+enum BuildReference {
+    /// !<output_name>!<drv_path>
+    SingleOutput(OutputName, DrvPath),
+
+    /// =<drv_path>
+    DrvClosure(DrvPath),
+
+    /// <path>
+    Path(StorePath),
+}
+```
+
+String contexts are, broadly speaking, created whenever a string is the result
+of a computation (e.g. string interpolation) that used a *computed* path or
+derivation in any way.
+
+Note: This explicitly does *not* include simply writing a literal string
+containing a store path (whether valid or not). That is only permitted through
+the `storePath` builtin.
+
+## Derivation inputs
+
+Based on the data above, the fields `inputDrvs` and `inputSrcs` of derivations
+are populated in `builtins.derivationStrict` (the function which
+`builtins.derivation`, which isn't actually a builtin, wraps).
+
+`inputDrvs` is represented by a map of derivation paths to the set of their
+outputs that were referenced by the context.
+
+TODO: What happens if the set is empty? Somebody claimed this means all outputs.
+
+`inputSrcs` is represented by a set of paths.
+
+These are populated by the above references as follows:
+
+* `SingleOutput` entries are merged into `inputDrvs`
+* `Path` entries are inserted into `inputSrcs`
+* `DrvClosure` leads to a special store computation (`computeFSClosure`), which
+  finds all paths referenced by the derivation and then inserts all of them into
+  the fields as above (derivations with _all_ their outputs)
+
+This is then serialised in the derivation and passed down the pipe.
+
+## Builtins interfacing with contexts
+
+C++ Nix has several builtins that interface directly with string contexts:
+
+* `unsafeDiscardStringContext`: throws away a string's string context (if
+  present)
+* `hasContext`: returns `true`/`false` depending on whether the string has
+  context
+* `unsafeDiscardOutputDependency`: drops dependencies on the *outputs* of a
+  `.drv` in the context, passing only the literal `.drv` itself
+
+  Note: This is only used for special test-cases in nixpkgs, and deprecated Nix
+  commands like `nix-push`.
+* `getContext`: returns the string context in serialised form as a Nix attribute
+  set
+* `appendContext`: adds a given string context to the string in the same format
+  as returned by `getContext`
+
+## Placeholders
+
+C++ Nix has `builtins.placeholder`, which given the name of an output (e.g.
+`out`) creates a hashed string representation of that output name. If that
+string is used anywhere in input attributes, the builder will replace it with
+the actual name of the corresponding output of the current derivation.
+
+C++ Nix does not use contexts for this, it blindly creates a rewrite map of
+these placeholder strings to the names of all outputs, and runs the output
+replacement logic on all environment variables it creates, attribute files it
+passes etc.
+
+## Tvix & string contexts
+
+Tvix does not track string contexts in its evaluator at all. Instead we are
+investigating implementing a system which allows us to drop string contexts in
+favour of reference scanning derivation attributes.
+
+This means that instead of maintaining and passing around a string context data
+structure in eval, we maintain a data structure of *known paths* from the same
+evaluation elsewhere in Tvix, and scan each derivation attribute against this
+set of known paths when instantiating derivations.
+
+Until proven otherwise, we take the stance that the system of string contexts as
+implemented in C++ Nix is likely an implementation detail that should not be
+leaking to the language surface as it does now.
+
+### Tracking "known paths"
+
+Every time a Tvix evaluation does something that causes a store interaction, a
+"known path" is created. On the language surface, this is the result of one of:
+
+1. Path literals (e.g. `src = ./.`).
+2. Calls to `builtins.derivationStrict` yielding a derivation and its output
+   paths.
+3. Calls to `builtins.path`.
+
+Whenever one of these occurs, some metadata that persists for the duration of
+one evaluation should be created in Nix. This metadata needs to be available in
+`builtins.derivationStrict`, and should be able to respond to these queries:
+
+1. What is the set of all known paths? (used for e.g. instantiating an
+   Aho-Corasick type string searcher)
+2. What is the _type_ of a path? (derivation path, derivation output, source
+   file)
+3. What are the outputs of a derivation?
+4. What is the derivation of an output?
+
+These queries will need to be asked of the metadata when populating the
+derivation fields.
+
+Note: Depending on how we implement `builtins.placeholder`, it might be useful
+to track created placeholders in this metadata, too.
+
+### Context builtins
+
+Context-reading builtins can be implemented in Tvix by adding `hasContext` and
+`getContext` with the appropriate reference-scanning logic. However, we should
+evaluate how these are used in nixpkgs and whether their uses can be removed.
+
+Context-mutating builtins can be implemented by tracking their effects in the
+value representation of Tvix, however we should consider not doing this at all.
+
+`unsafeDiscardOutputDependency` should probably never be used and we should warn
+or error on it.
+
+`unsafeDiscardStringContext` is often used as a workaround for avoiding IFD in
+inconvenient places (e.g. in the TVL depot pipeline generation). This is
+unnecessary in Tvix. We should evaluate which other uses exist, and act on them
+appropriately.
+
+The initial danger with diverging here is that we might cause derivation hash
+discrepancies between Tvix and C++ Nix, which can make initial comparisons of
+derivations generated by the two systems difficult. If this occurs we need to
+discuss how to approach it, but initially we will implement the mutating
+builtins as no-ops.
diff --git a/tvix/eval/docs/builtins.md b/tvix/eval/docs/builtins.md
new file mode 100644
index 000000000000..00af50484903
--- /dev/null
+++ b/tvix/eval/docs/builtins.md
@@ -0,0 +1,138 @@
+Nix builtins
+============
+
+Nix has a lot of built-in functions, some of which are accessible in
+the global scope, and some of which are only accessible through the
+global `builtins` attribute set.
+
+This document is an attempt to track all of these builtins, but
+without documenting their functionality.
+
+See also https://nixos.org/manual/nix/stable/expressions/builtins.html
+
+The `impl` column indicates implementation status in tvix:
+- implemented: "" (empty cell)
+- not yet implemented, but not blocked: `todo`
+- not yet implemented, but blocked by other prerequisites:
+  - `store`: awaiting eval<->store api(s)
+  - `context`: awaiting support for string contexts
+
+| name                          | global | arity | pure  | impl    |
+|-------------------------------|--------|-------|-------|---------|
+| abort                         | true   | 1     |       |         |
+| add                           | false  | 2     | true  |         |
+| addErrorContext               | false  | ?     |       | context |
+| all                           | false  | 2     | true  |         |
+| any                           | false  | 2     | true  |         |
+| appendContext                 | false  | ?     |       | context |
+| attrNames                     | false  | 1     | true  |         |
+| attrValues                    | false  |       | true  |         |
+| baseNameOf                    | true   |       |       |         |
+| bitAnd                        | false  |       |       |         |
+| bitOr                         | false  |       |       |         |
+| bitXor                        | false  |       |       |         |
+| builtins                      | true   |       |       |         |
+| catAttrs                      | false  |       |       |         |
+| compareVersions               | false  |       |       |         |
+| concatLists                   | false  |       |       |         |
+| concatMap                     | false  |       |       |         |
+| concatStringsSep              | false  |       |       |         |
+| currentSystem                 | false  |       |       |         |
+| currentTime                   | false  |       | false |         |
+| deepSeq                       | false  |       |       |         |
+| derivation                    | true   |       |       | store   |
+| derivationStrict              | true   |       |       | store   |
+| dirOf                         | true   |       |       |         |
+| div                           | false  |       |       |         |
+| elem                          | false  |       |       |         |
+| elemAt                        | false  |       |       |         |
+| false                         | true   |       |       |         |
+| fetchGit                      | true   |       |       | store   |
+| fetchMercurial                | true   |       |       | store   |
+| fetchTarball                  | true   |       |       | store   |
+| fetchurl                      | false  |       |       | store   |
+| filter                        | false  |       |       |         |
+| filterSource                  | false  |       |       | store   |
+| findFile                      | false  |       | false | todo    |
+| foldl'                        | false  |       |       |         |
+| fromJSON                      | false  |       |       |         |
+| fromTOML                      | true   |       |       |         |
+| functionArgs                  | false  |       |       |         |
+| genList                       | false  |       |       |         |
+| genericClosure                | false  |       |       | todo    |
+| getAttr                       | false  |       |       |         |
+| getContext                    | false  |       |       | context |
+| getEnv                        | false  |       | false |         |
+| hasAttr                       | false  |       |       |         |
+| hasContext                    | false  |       |       | context |
+| hashFile                      | false  |       | false | todo    |
+| hashString                    | false  |       |       | todo    |
+| head                          | false  |       |       |         |
+| import                        | true   |       |       |         |
+| intersectAttrs                | false  |       |       |         |
+| isAttrs                       | false  |       |       |         |
+| isBool                        | false  |       |       |         |
+| isFloat                       | false  |       |       |         |
+| isFunction                    | false  |       |       |         |
+| isInt                         | false  |       |       |         |
+| isList                        | false  |       |       |         |
+| isNull                        | true   |       |       |         |
+| isPath                        | false  |       |       |         |
+| isString                      | false  |       |       |         |
+| langVersion                   | false  |       |       |         |
+| length                        | false  |       |       |         |
+| lessThan                      | false  |       |       |         |
+| listToAttrs                   | false  |       |       |         |
+| map                           | true   |       |       |         |
+| mapAttrs                      | false  |       |       |         |
+| match                         | false  |       |       |         |
+| mul                           | false  |       |       |         |
+| nixPath                       | false  |       |       | todo    |
+| nixVersion                    | false  |       |       | todo    |
+| null                          | true   |       |       |         |
+| parseDrvName                  | false  |       |       |         |
+| partition                     | false  |       |       |         |
+| path                          | false  |       | sometimes | store |
+| pathExists                    | false  |       | false |         |
+| placeholder                   | true   |       |       | context |
+| readDir                       | false  |       | false |         |
+| readFile                      | false  |       | false |         |
+| removeAttrs                   | true   |       |       |         |
+| replaceStrings                | false  |       |       |         |
+| scopedImport                  | true   |       |       |         |
+| seq                           | false  |       |       |         |
+| sort                          | false  |       |       |         |
+| split                         | false  |       |       |         |
+| splitVersion                  | false  |       |       |         |
+| storeDir                      | false  |       |       | store   |
+| storePath                     | false  |       |       | store   |
+| stringLength                  | false  |       |       |         |
+| sub                           | false  |       |       |         |
+| substring                     | false  |       |       |         |
+| tail                          | false  |       |       |         |
+| throw                         | true   |       |       |         |
+| toFile                        | false  |       |       | store   |
+| toJSON                        | false  |       |       | todo    |
+| toPath                        | false  |       |       |         |
+| toString                      | true   |       |       |         |
+| toXML                         | false  |       |       | todo    |
+| trace                         | false  |       |       |         |
+| true                          | true   |       |       |         |
+| tryEval                       | false  |       |       |         |
+| typeOf                        | false  |       |       |         |
+| unsafeDiscardOutputDependency | false  |       |       | context |
+| unsafeDiscardStringContext    | false  |       |       | context |
+| unsafeGetAttrPos              | false  |       |       | todo    |
+| valueSize                     | false  |       |       | todo    |
+
+## Added after C++ Nix 2.3 (without Flakes enabled)
+
+| name          | global | arity | pure  | impl  |
+|---------------|--------|-------|-------|-------|
+| break         | false  | 1     |       | todo  |
+| ceil          | false  | 1     | true  |       |
+| fetchTree     | true   | 1     |       | todo  |
+| floor         | false  | 1     | true  |       |
+| groupBy       | false  | 2     | true  |       |
+| traceVerbose  | false  | 2     |       | todo  |
+| zipAttrsWith  | false  | 2     | true  | todo  |
diff --git a/tvix/eval/docs/known-optimisation-potential.md b/tvix/eval/docs/known-optimisation-potential.md
new file mode 100644
index 000000000000..f45f1ee6c48a
--- /dev/null
+++ b/tvix/eval/docs/known-optimisation-potential.md
@@ -0,0 +1,130 @@
+Known Optimisation Potential
+============================
+
+There are several areas of the Tvix evaluator code base where
+potentially large performance gains can be achieved through
+optimisations that we are already aware of.
+
+The shape of most optimisations is that of moving more work into the
+compiler to simplify the runtime execution of Nix code. This leads, in
+some cases, to drastically higher complexity in both the compiler
+itself and in invariants that need to be guaranteed between the
+runtime and the compiler.
+
+For this reason, and because we lack the infrastructure to adequately
+track their impact (WIP), we have not yet implemented these
+optimisations, but note the most important ones here.
+
+* Use "open upvalues" [hard]
+
+  Right now, Tvix will immediately close over all upvalues that are
+  created and clone them into the `Closure::upvalues` array.
+
+  Instead of doing this, we can statically determine most locals that
+  are closed over *and escape their scope* (similar to how the
+  `compiler::scope::Scope` struct currently tracks whether locals are
+  used at all).
+
+  If we implement the machinery to track this, we can implement some
+  upvalues at runtime by simply sticking stack indices in the upvalue
+  array and only copy the values where we know that they escape.
+
+* Avoid `with` value duplication [easy]
+
+  If a `with` makes use of a local identifier in a scope that can not
+  close before the with (e.g. not across `LambdaCtx` boundaries), we
+  can avoid the allocation of the phantom value and duplication of the
+  `NixAttrs` value on the stack. In this case we simply push the stack
+  index of the known local.
+
+* Multiple attribute selection [medium]
+
+  An instruction could be introduced that avoids repeatedly pushing an
+  attribute set to/from the stack if multiple keys are being selected
+  from it. This occurs, for example, when inheriting from an attribute
+  set or when binding function formals.
+
+* Split closure/function representation [easy]
+
+  Functions have fewer fields that need to be populated at runtime and
+  can directly use the `value::function::Lambda` representation where
+  possible.
+
+* Apply `compiler::optimise_select` to other set operations [medium]
+
+  In addition to selects, statically known attribute resolution could
+  also be used for things like `?` or `with`. The latter might be a
+  little more complicated but is worth investigating.
+
+* Inline fully applied builtins with equivalent operators [medium]
+
+  Some `builtins` have equivalent operators, e.g. `builtins.sub`
+  corresponds to the `-` operator, `builtins.hasAttr` to the `?`
+  operator etc. These operators additionally compile to a primitive
+  VM opcode, so they should be just as cheap (if not cheaper) as
+  a builtin application.
+
+  In case the compiler encounters a fully applied builtin (i.e.
+  no currying is occurring) and the `builtins` global is unshadowed,
+  it could compile the equivalent operator bytecode instead: For
+  example, `builtins.sub 20 22` would be compiled as `20 - 22`.
+  This would ensure that equivalent `builtins` can also benefit
+  from special optimisations we may implement for certain operators
+  (in the absence of currying). E.g. we could optimise access
+  to the `builtins` attribute set which a call to
+  `builtins.getAttr "foo" builtins` should also profit from.
+
+* Avoid nested `VM::run` calls [hard]
+
+  Currently when encountering Nix-native callables (thunks, closures)
+  the VM's run loop will nest and return the value of the nested call
+  frame one level up. This makes the Rust call stack almost mirror the
+  Nix call stack, which is usually undesirable.
+
+  It is possible to detect situations where this is avoidable and
+  instead set up the VM in such a way that it continues and produces
+  the desired result in the same run loop, but this is kind of tricky
+  to get right - especially while other parts are still in flux.
+
+  For details consult the commit with Gerrit change ID
+  `I96828ab6a628136e0bac1bf03555faa4e6b74ece`, in which the initial
+  attempt at doing this was reverted.
+
+* Avoid thunks if only identifier closing is required [medium]
+
+  Some constructs, like `with`, mostly do not change runtime behaviour
+  if thunked. However, they are wrapped in thunks to ensure that
+  deferred identifiers are resolved correctly.
+
+  This can be avoided, as we statically analyse the scope and should
+  be able to tell whether any such logic was required.
+
+* Intern literals [easy]
+
+  Currently, the compiler emits a separate entry in the constant
+  table for each literal.  So the program `1 + 1 + 1` will have
+  three entries in its `Chunk::constants` instead of only one.
+
+* Do some list and attribute set operations in place [hard]
+
+  Algorithms that can not do a lot of work inside `builtins` like `map`,
+  `filter` or `foldl'` usually perform terribly if they use data structures like
+  lists and attribute sets.
+
+  `builtins` can do work in place on a copy of a `Value`, but naïvely expressed
+  recursive algorithms will usually use `//` and `++` to do a single change to a
+  `Value` at a time, requiring a full copy of the data structure each time.
+  It would be a big improvement if we could do some of these operations in place
+  without requiring a new copy.
+
+  There are probably two approaches: We could determine statically if a value is
+  reachable from elsewhere and emit a special in place instruction if not. An
+  easier alternative is probably to rely on reference counting at runtime: If no
+  other reference to a value exists, we can extend the list or update the
+  attribute set in place.
+
+  An **alternative** to this is using [persistent data
+  structures](https://en.wikipedia.org/wiki/Persistent_data_structure) or at the
+  very least [immutable data structures](https://docs.rs/im/latest/im/) that can
+  be copied more efficiently than the stock structures we are using at the
+  moment.
diff --git a/tvix/eval/docs/language-issues.md b/tvix/eval/docs/language-issues.md
new file mode 100644
index 000000000000..26401665bbb5
--- /dev/null
+++ b/tvix/eval/docs/language-issues.md
@@ -0,0 +1,46 @@
+# Nix language issues
+
+In the absence of a language standard, what Nix (the language) is, is prescribed
+by the behavior of the C++ Nix implementation. Still, there are reasons not to
+accept some behavior:
+
+* Tvix aims for nixpkgs compatibility only. This means we can ignore behavior in
+  edge cases nixpkgs doesn't trigger as well as obscure features it doesn't use
+  (e.g. `__overrides`).
+* Some behavior of the Nix evaluator seems to be unintentional or an
+  implementation detail leaking out into language behavior.
+
+Especially in the latter case, it makes sense to raise the respective issue and
+maybe to get rid of the behavior in all implementations for good. Below is an
+(incomplete) list of such issues:
+
+* [Behaviour of nested attribute sets depends on definition order][i7111]
+* [Partially constructed attribute sets are observable during dynamic attr names construction][i7012]
+* [Nix parsers merges multiple attribute set literals for the same key incorrectly depending on definition order](i7115)
+
+On the other hand, there is behavior that seems to violate one's expectation
+about the language at first, but has good enough reasons from an implementor's
+perspective to keep them:
+
+* Dynamic keys are forbidden in `let` and `inherit`. This makes sure that we
+  only need to do runtime identifier lookups for `with`. More dynamic (i.e.
+  runtime) lookups would make the scoping system even more complicated as well
+  as hurt performance.
+* Dynamic attributes of `rec` sets are not added to its scope. This makes sense
+  for the same reason.
+* Dynamic and nested attributes in attribute sets don't get merged. This is a
+  tricky one, but avoids doing runtime (recursive) merges of attribute sets.
+  Instead all necessary merging can be inferred statically, i.e. the C++ Nix
+  implementation already merges at parse time, making nested attribute keys
+  syntactic sugar effectively.
+
+Other behavior is just odd, surprising or underdocumented:
+
+* `builtins.foldl'` doesn't force the initial accumulator (but all other
+  intermediate accumulator values), differing from e.g. Haskell, see
+  the [relevant PR discussion](p7158).
+
+[i7111]: https://github.com/NixOS/nix/issues/7111
+[i7012]: https://github.com/NixOS/nix/issues/7012
+[i7115]: https://github.com/NixOS/nix/issues/7115
+[p7158]: https://github.com/NixOS/nix/pull/7158
diff --git a/tvix/eval/docs/opcodes-attrsets.md b/tvix/eval/docs/opcodes-attrsets.md
new file mode 100644
index 000000000000..7026f3319dda
--- /dev/null
+++ b/tvix/eval/docs/opcodes-attrsets.md
@@ -0,0 +1,122 @@
+# attrset-opcodes
+
+The problem with attrset literals is twofold:
+
+1. The keys of attribute sets may be dynamically evaluated.
+
+   Access:
+
+   ```nix
+   let
+     k = "foo";
+     attrs = { /* etc. */ };
+   in attrs."${k}"
+   ```
+
+   Literal:
+   ```nix
+   let
+     k = "foo";
+   in {
+     "${k}" = 42;
+   }
+   ```
+
+   The problem with this is that the attribute set key is not known at
+   compile time, and needs to be dynamically evaluated by the VM as an
+   expression.
+
+   For the most part this should be pretty simple, assuming a
+   theoretical instruction set:
+
+   ```
+   0000  OP_CONSTANT(0) # key "foo"
+   0001  OP_CONSTANT(1) # value 42
+   0002  OP_ATTR_SET(1) # construct attrset from 2 stack values
+   ```
+
+   The operation pushing the key needs to be replaced with one that
+   leaves a single value (the key) on the stack, i.e. the code for the
+   expression, e.g.:
+
+   ```
+   0000..000n <operations leaving a string value on the stack>
+   000n+1     OP_CONSTANT(1) # value 42
+   000n+2     OP_ATTR_SET(1) # construct attrset from 2 stack values
+   ```
+
+   This is fairly easy to do by simply recursing in the compiler when
+   the key expression is encountered.
+
+2. The keys of attribute sets may be nested.
+
+   This is the non-trivial part of dealing with attribute set
+   literals. Specifically, the nesting can be arbitrarily deep and the
+   AST does not guarantee that related set keys are located
+   adjacently.
+
+   Furthermore, this frequently occurs in practice in Nix. We need a
+   bytecode representation that makes it possible to construct nested
+   attribute sets at runtime.
+
+   Proposal: AttrPath values
+
+   If we can leave a value representing an attribute path on the
+   stack, we can offload the construction of nested attribute sets to
+   the `OpAttrSet` operation.
+
+   Under the hood, OpAttrSet in practice constructs a `Map<NixString,
+   Value>` attribute set in most cases. This means it expects to pop
+   the value of the key of the stack, but is otherwise free to do
+   whatever it wants with the underlying map.
+
+   In a simple example, we could have code like this:
+
+   ```nix
+   {
+     a.b = 15;
+   }
+   ```
+
+   This would be compiled to a new `OpAttrPath` instruction that
+   constructs and pushes an attribute path from a given number of
+   fragments (which are popped off the stack).
+
+   For example,
+
+   ```
+   0000 OP_CONSTANT(0)  # key "a"
+   0001 OP_CONSTANT(1)  # key "b"
+   0002 OP_ATTR_PATH(2) # construct attrpath from 2 fragments
+   0003 OP_CONSTANT(2)  # value 42
+   0004 OP_ATTRS(1)     # construct attrset from one pair
+   ```
+
+   Right before `0004` the stack would be left like this:
+
+   [ AttrPath[a,b], 42 ]
+
+   Inside of the `OP_ATTRS` instruction we could then begin
+   construction of the map and insert the nested attribute sets as
+   required, as well as validate that there are no duplicate keys.
+
+3. Both of these cases can occur simultaneously, but this is not a
+   problem as the opcodes combine perfectly fine, e.g.:
+
+   ```nix
+   let
+     k = "a";
+   in {
+     "${k}".b = 42;
+   }
+   ```
+
+   results in
+
+   ```
+   0000..000n <operations leaving a string value on the stack>
+   000n+1     OP_CONSTANT(1)  # key "b"
+   000n+2     OP_ATTR_PATH(2) # construct attrpath from 2 fragments
+   000n+3     OP_CONSTANT(2)  # value 42
+   000n+4     OP_ATTR_SET(1)  # construct attrset from 2 stack values
+   ```
diff --git a/tvix/eval/docs/recursive-attrs.md b/tvix/eval/docs/recursive-attrs.md
new file mode 100644
index 000000000000..c30cfd33e6c7
--- /dev/null
+++ b/tvix/eval/docs/recursive-attrs.md
@@ -0,0 +1,68 @@
+Recursive attribute sets
+========================
+
+The construction behaviour of recursive attribute sets is very
+specific, and a bit peculiar.
+
+In essence, there are multiple "phases" of scoping that take place
+during attribute set construction:
+
+1. Every inherited value without an explicit source is inherited only
+   from the **outer** scope in which the attribute set is enclosed.
+
+2. A new scope is opened in which all recursive keys are evaluated.
+   This only considers **statically known keys**, attributes can
+   **not** recurse into dynamic keys in `self`!
+
+   For example, this code is invalid in C++ Nix:
+
+   ```
+   nix-repl> rec { ${"a"+""} = 2; b = a * 10; }
+   error: undefined variable 'a' at (string):1:26
+   ```
+
+3. Finally, a third scope is opened in which dynamic keys are
+   evaluated.
+
+This behaviour, while possibly a bit strange and unexpected, actually
+simplifies the implementation of recursive attribute sets in Tvix as
+well.
+
+Essentially, a recursive attribute set like this:
+
+```nix
+rec {
+  inherit a;
+  b = a * 10;
+  ${"c" + ""} = b * 2;
+}
+```
+
+Can be compiled like the following expression:
+
+```nix
+let
+  inherit a;
+in let
+  b = a * 10;
+  in {
+    inherit a b;
+    ${"c" + ""} = b * 2;
+  }
+```
+
+Completely deferring the resolution of recursive identifiers to the
+existing handling of recursive scopes (i.e. deferred access) in let
+bindings.
+
+In practice, we can further specialise this and compile each scope
+directly into the form expected by `OpAttrs` (that is, leaving
+attribute names on the stack) before each value's position.
+
+C++ Nix's Implementation
+------------------------
+
+* [`ExprAttrs`](https://github.com/NixOS/nix/blob/2097c30b08af19a9b42705fbc07463bea60dfb5b/src/libexpr/nixexpr.hh#L241-L268)
+  (AST representation of attribute sets)
+* [`ExprAttrs::eval`](https://github.com/NixOS/nix/blob/075bf6e5565aff9fba0ea02f3333c82adf4dccee/src/libexpr/eval.cc#L1333-L1414)
+* [`addAttr`](https://github.com/NixOS/nix/blob/master/src/libexpr/parser.y#L98-L156) (`ExprAttrs` construction in the parser)
diff --git a/tvix/eval/docs/vm-loop.md b/tvix/eval/docs/vm-loop.md
new file mode 100644
index 000000000000..6266d34709cb
--- /dev/null
+++ b/tvix/eval/docs/vm-loop.md
@@ -0,0 +1,315 @@
+tvix-eval VM loop
+=================
+
+This document describes the new tvix-eval VM execution loop implemented in the
+chain focusing around cl/8104.
+
+## Background
+
+The VM loop implemented in Tvix prior to cl/8104 had several functions:
+
+1. Advancing the instruction pointer for a chunk of Tvix bytecode and
+   executing instructions in a loop until a result was yielded.
+
+2. Tracking Nix call frames as functions/thunks were entered/exited.
+
+3. Catching trampoline requests returned from instructions to force suspended
+   thunks without increasing stack size *where possible*.
+
+4. Handling trampolines through an inner trampoline loop, switching between a
+   code execution mode and execution of subsequent trampolines.
+
+This implementation of the trampoline logic was added on to the existing VM,
+which previously always recursed for thunk forcing. There are some cases (for
+example values that need to be forced *inside* of the execution of a builtin)
+where trampolines could not previously be used, and the VM recursed anyways.
+
+As a result of this trampoline logic being added "on top" of the existing VM
+loop the code became quite difficult to understand. This led to several bugs,
+for example: b/251, b/246, b/245, and b/238.
+
+These bugs were tricky to deal with, as we had to try and make the VM do
+things that are somewhat difficult to fit into its model. We could of course
+keep extending the trampoline logic to accommodate all sorts of concepts (such
+as finalisers), but that seems like it does not solve the root problem.
+
+## New VM loop
+
+In cl/8104, a unified new solution is implemented with which the VM is capable
+of evaluating everything without increasing the call stack size.
+
+This is done by introducing a new frame stack in the VM, on which execution
+frames are enqueued that are either:
+
+1. A bytecode frame, consisting of Tvix bytecode that evaluates compiled Nix
+   code.
+2. A generator frame, consisting of some VM logic implemented in pure Rust
+   code that can be *suspended* when it hits a point where the VM would
+   previously need to recurse.
+
+We do this by making use of the `async` *keyword* in Rust, but notably
+*without* introducing asynchronous I/O or concurrency in tvix-eval (the
+complexity of which is currently undesirable for us).
+
+Specifically, when writing a Rust function that uses the `async` keyword, such
+as:
+
+```rust
+async fn some_builtin(input: Value) -> Result<Value, ErrorKind> {
+  let mut out = NixList::new();
+
+  for element in input.to_list()? {
+    let result = do_something_that_requires_the_vm(element).await;
+    out.push(result);
+  }
+
+  Ok(out)
+}
+```
+
+The compiler actually generates a state-machine under-the-hood which allows
+the execution of that function to be *suspended* whenever it hits an `await`.
+
+We use the [`genawaiter`][] crate that gives us a data structure and simple
+interface for getting instances of these state machines that can be stored in
+a struct (in our case, a *generator frame*).
+
+The execution of the VM then becomes the execution of an *outer loop*, which
+is responsible for selecting the next generator frame to execute, and two
+*inner loops*, which drive the execution of a bytecode frame or generator
+frame forward until it either yields a value or asks to be suspended in favour
+of another frame.
+
+All "communication" between frames happens solely through values left on the
+stack: Whenever a frame of either type runs to completion, it is expected to
+leave a *single* value on the stack. It follows that the whole VM, upon
+completion of the last (or initial, depending on your perspective) frame
+yields its result as the return value.
+
+The core of the VM restructuring is cl/8104, unfortunately one of the largest
+single commit changes we've had to make yet, as it touches pretty much all
+areas of tvix-eval. The introduction of the generators and the
+message/response system we built to request something from the VM, suspend a
+generator, and wait for the return is in cl/8148.
+
+The next sections describe in detail how the three different loops work.
+
+### Outer VM loop
+
+The outer VM loop is responsible for selecting the next frame to run, and
+dispatching it correctly to inner loops, as well as determining when to shut
+down the VM and return the final result.
+
+```
+                          ╭──────────────────╮
+                 ╭────────┤ match frame kind ├──────╮
+                 │        ╰──────────────────╯      │
+                 │                                  │
+    ┏━━━━━━━━━━━━┷━━━━━┓                ╭───────────┴───────────╮
+───►┃ frame_stack.pop()┃                ▼                       ▼
+    ┗━━━━━━━━━━━━━━━━━━┛       ┏━━━━━━━━━━━━━━━━┓      ┏━━━━━━━━━━━━━━━━━┓
+                 ▲             ┃ bytecode frame ┃      ┃ generator frame ┃
+                 │             ┗━━━━━━━━┯━━━━━━━┛      ┗━━━━━━━━┯━━━━━━━━┛
+                 │[yes, cont.]          │                       │
+                 │                      ▼                       ▼
+    ┏━━━━━━━━┓   │             ╔════════════════╗      ╔═════════════════╗
+◄───┨ return ┃   │             ║ inner bytecode ║      ║ inner generator ║
+    ┗━━━━━━━━┛   │             ║      loop      ║      ║      loop       ║
+        ▲        │             ╚════════╤═══════╝      ╚════════╤════════╝
+        │   ╭────┴─────╮                │                       │
+        │   │ has next │                ╰───────────┬───────────╯
+   [no] ╰───┤  frame?  │                            │
+            ╰────┬─────╯                            ▼
+                 │                         ┏━━━━━━━━━━━━━━━━━┓
+                 │                         ┃ frame completed ┃
+                 ╰─────────────────────────┨  or suspended   ┃
+                                           ┗━━━━━━━━━━━━━━━━━┛
+```
+
+Initially, the VM always pops a frame from the frame stack and then inspects
+the type of frame it found. As a consequence the next frame to execute is
+always the frame at the top of the stack, and setting up a VM initially for
+code execution is done by leaving a bytecode frame with the code to execute on
+the stack and passing control to the outer loop.
+
+Control is dispatched to either of the inner loops (depending on the type of
+frame) and the cycle continues once they return.
+
+When an inner loop returns, it has either finished its execution (and left its
+result value on the *value stack*), or its frame has requested to be
+suspended.
+
+Frames request suspension by re-enqueueing *themselves* through VM helper
+methods, and then leaving the frame they want to run *on top* of themselves in
+the frame stack before yielding control back to the outer loop.
+
+The inner control loops inform the outer loops about whether the frame has
+been *completed* or *suspended* by returning a boolean.
+
+### Inner bytecode loop
+
+The inner bytecode loop drives the execution of some Tvix bytecode by
+continously looking at the next instruction to execute, and dispatching to the
+instruction handler.
+
+```
+   ┏━━━━━━━━━━━━━┓
+◄──┨ return true ┃
+   ┗━━━━━━━━━━━━━┛
+          ▲
+     ╔════╧═════╗
+     ║ OpReturn ║
+     ╚══════════╝
+          ▲
+          ╰──┬────────────────────────────╮
+             │                            ▼
+             │                 ╔═════════════════════╗
+    ┏━━━━━━━━┷━━━━━┓           ║ execute instruction ║
+───►┃ inspect next ┃           ╚══════════╤══════════╝
+    ┃  instruction ┃                      │
+    ┗━━━━━━━━━━━━━━┛                      │
+             ▲                      ╭─────┴─────╮
+             ╰──────────────────────┤ suspends? │
+                       [no]         ╰─────┬─────╯
+                                          │
+                                          │
+   ┏━━━━━━━━━━━━━━┓                       │
+◄──┨ return false ┃───────────────────────╯
+   ┗━━━━━━━━━━━━━━┛              [yes]
+```
+
+With this refactoring, the compiler now emits a special `OpReturn` instruction
+at the end of bytecode chunks. This is a signal to the runtime that the chunk
+has completed and that its current value should be returned, without having to
+perform instruction pointer arithmetic.
+
+When `OpReturn` is encountered, the inner bytecode loop returns control to the
+outer loop and informs it (by returning `true`) that the bytecode frame has
+completed.
+
+Any other instruction may also request a suspension of the bytecode frame (for
+example, instructions that need to force a value). In this case the inner loop
+is responsible for setting up the frame stack correctly, and returning `false`
+to inform the outer loop of the suspension
+
+### Inner generator loop
+
+The inner generator loop is responsible for driving the execution of a
+generator frame by continously calling [`Gen::resume`][] until it requests a
+suspension (as a result of which control is returned to the outer loop), or
+until the generator is done and yields a value.
+
+```
+   ┏━━━━━━━━━━━━━┓
+◄──┨ return true ┃ ◄───────────────────╮
+   ┗━━━━━━━━━━━━━┛                     │
+                                       │
+                               [Done]  │
+                    ╭──────────────────┴─────────╮
+                    │ inspect generator response │◄────────────╮
+                    ╰──────────────────┬─────────╯             │
+                            [yielded]  │              ┏━━━━━━━━┷━━━━━━━━┓
+                                       │              ┃ gen.resume(msg) ┃◄──
+                                       ▼              ┗━━━━━━━━━━━━━━━━━┛
+                                 ╭────────────╮                ▲
+                                 │ same-frame │                │
+                                 │  request?  ├────────────────╯
+                                 ╰─────┬──────╯      [yes]
+   ┏━━━━━━━━━━━━━━┓                    │
+◄──┨ return false ┃ ◄──────────────────╯
+   ┗━━━━━━━━━━━━━━┛                [no]
+```
+
+On each execution of a generator frame, `resume_with` is called with a
+[`VMResponse`][] (i.e. a message *from* the VM *to* the generator). For a newly
+created generator, the initial message is just `Empty`.
+
+A generator may then respond by signaling that it has finished execution
+(`Done`), in which case the inner generator loop returns control to the outer
+loop and informs it that this generator is done (by returning `true`).
+
+A generator may also respond by signaling that it needs some data from the VM.
+This is implemented through a request-response pattern, in which the generator
+returns a `Yielded` message containing a [`VMRequest`][]. These requests can be
+very simple ("Tell me the current store path") or more complex ("Call this Nix
+function with these values").
+
+Requests are divided into two classes: Same-frame requests (requests that can be
+responded to *without* returning control to the outer loop, i.e. without
+executing a *different* frame), and multi-frame generator requests. Based on the
+type of request, the inner generator loop will either handle it right away and
+send the response in a new `resume_with` call, or return `false` to the outer
+generator loop after setting up the frame stack.
+
+Most of this logic is implemented in cl/8148.
+
+[`Gen::resume`]: https://docs.rs/genawaiter/0.99.1/genawaiter/rc/struct.Gen.html#method.resume_with
+[`VMRequest`]: https://cs.tvl.fyi/depot@2696839770c1ccb62929ff2575a633c07f5c9593/-/blob/tvix/eval/src/vm/generators.rs?L44
+[`VMResponse`]: https://cs.tvl.fyi/depot@2696839770c1ccb62929ff2575a633c07f5c9593/-/blob/tvix/eval/src/vm/generators.rs?L169
+
+## Advantages & Disadvantages of the approach
+
+This approach has several advantages:
+
+* The execution model is much simpler than before, making it fairly
+  straightforward to build up a mental model of what the VM does.
+
+* All "out of band requests" inside the VM are handled through the same
+  abstraction (generators).
+
+* Implementation is not difficult, albeit a little verbose in some cases (we
+  can argue about whether or not to introduce macros for simplifying it).
+
+* Several parts of the VM execution are now much easier to document,
+  potentially letting us onboard tvix-eval contributors faster.
+
+* The linear VM execution itself is much easier to trace now, with for example
+  the `RuntimeObserver` (and by extension `tvixbolt`) giving much clearer
+  output now.
+
+But it also comes with some disadvantages:
+
+* Even though we "only" use the `async` keyword without a full async-I/O
+  runtime, we still encounter many of the drawbacks of the fragmented Rust
+  async ecosystem.
+
+  The biggest issue with this is that parts of the standard library become
+  unavailable to us, for example the built-in `Vec::sort_by` can no longer be
+  used for sorting in Nix because our comparators themselves are `async`.
+
+  This led us to having to implement some logic on our own, as the design of
+  `async` in Rust even makes it difficult to provide usecase-generic
+  implementations of concepts like sorting.
+
+* We need to allocate quite a few new structures on the heap in order to drive
+  generators, as generators involve storing `Future` types (with unknown
+  sizes) inside of structs.
+
+  In initial testing this seems to make no significant difference in
+  performance (our performance in an actual nixpkgs-eval is still bottlenecked
+  by I/O concerns and reference scanning), but is something to keep in mind
+  later on when we start optimising more after the low-hanging fruits have
+  been reaped.
+
+## Alternatives considered
+
+1. Tacking on more functionality onto the existing VM loop
+   implementation to accomodate problems as they show up. This is not
+   preferred as the code is already getting messy.
+
+2. Making tvix-eval a fully `async` project, pulling in something like Tokio
+   or `async-std` as a runtime. This is not preferred due to the massively
+   increased complexity of those solutions, and all the known issues of fully
+   buying in to the async ecosystem.
+
+   tvix-eval fundamentally should work for use-cases besides building Nix
+   packages (e.g. for `//tvix/serde`), and its profile should be as slim as
+   possible.
+
+3. Convincing the Rust developers that Rust needs a way to guarantee
+   constant-stack-depth tail calls through something like a `tailcall`
+   keyword.
+
+4. ... ?
+
+[`genawaiter`]: https://docs.rs/genawaiter/
diff --git a/tvix/eval/proptest-regressions/value/mod.txt b/tvix/eval/proptest-regressions/value/mod.txt
new file mode 100644
index 000000000000..05b01b4c768b
--- /dev/null
+++ b/tvix/eval/proptest-regressions/value/mod.txt
@@ -0,0 +1,10 @@
+# Seeds for failure cases proptest has generated in the past. It is
+# automatically read and these particular cases re-run before any
+# novel cases are generated.
+#
+# It is recommended to check this file in to source control so that
+# everyone who runs the test benefits from these saved cases.
+cc 241ec68db9f684f4280d4c7907f7105e7b746df433fbb5cbd6bf45323a7f3be0 # shrinks to input = _ReflexiveArgs { x: List(NixList([List(NixList([Path("𑁯")]))])) }
+cc b6ab5fb25f5280f39d2372e951544d8cc9e3fcd5da83351266a0a01161e12dd7 # shrinks to input = _ReflexiveArgs { x: Attrs(NixAttrs(KV { name: Path("𐎸-{\u{a81}lq9Z"), value: Bool(false) })) }
+cc 3656053e7a8dbe1c01dd68a8e06840fb6e693dde942717a7c18173876d9c2cce # shrinks to input = _SymmetricArgs { x: List(NixList([Path("\u{1daa2}:.H🢞ୡ\\🕴7iu𝋮T𝓕\\%i:"), Integer(-7435423970896550032), Float(1.2123587650724335e-5), Integer(5314432620816586712), Integer(-8316092768376026052), Integer(-7632521684027819842), String(NixString(Smol("ᰏ᥀\\\\ȷf8=\u{2003}\"𑁢רּA/%�bQffl<౯Ⱥ\u{1b3a}`T{"))), Bool(true), String(NixString(Smol("Ⱥힳ\"<\\`tZ/�൘🢦Ⱥ=x𑇬"))), Null, Bool(false), String(NixString(Smol(";%'࿎.ೊ🉡𑌊/🕴3Ja"))), Null, String(NixString(Smol("vNᛦ=\\`𝐓P\\"))), Bool(true), Null, String(NixString(Smol("\"zાë\u{11cb6}6%yꟽ𚿾🡖`!"))), Integer(1513983844724992869), Bool(true), Float(-8.036903674864022e114), Path("G࿐�/᠆₫%𝈇P"), Bool(false), Bool(true), Null, Attrs(NixAttrs(Empty)), Float(-6.394835856260315e-46), Null, Path("G?🭙O<🟰𐮭𑤳�*ܥ𞹉`?$/j1=p𑙕h\u{e0147}\u{1cf3b}"), Bool(false), Bool(false), Path("$"), Float(7.76801238087078e-309), Integer(-4304837936532390878), Attrs(NixAttrs(Im({NixString(Smol("")): Float(-1.5117468628684961e-307), NixString(Smol("#=B\"o~Ѩ\"Ѩ𐠅T")): String(NixString(Smol("Kঅ&NVꩋࠔ'𝄏<"))), NixString(Heap("&¥sଲ\"\\=𑖺Q𚿾VTຐ[ﬓ%")): Integer(-9079075359788064855), NixString(Smol("'``{:5𑚞=l𑣿")): Bool(true), NixString(Smol("*𐖚")): Bool(false), NixString(Heap(":*𐖛𑵨%C'Ѩ")): Null, NixString(Smol("?ծ/෴")): Bool(true), NixString(Smol("W𑌏")): Float(-1.606122565666547e-309), NixString(Smol("`𐺭¥\u{9d7}𖿡1Y𖤛>")): Float(0.0), NixString(Heap("{&]\\𞅂𞅅&7ସ")): Path("\"*o$ଇ🛢𞸤�🉐�🃏?##bﷅ|a¥࿔ᓊ\u{bd7}𑆍W?P𑊌𑩰"), NixString(Smol("Ⱥ\u{10a3a}a/🕴'\u{b3e}𐦨Sᅩ$1kw\u{cd5}B)\u{e01b7}1:R@")): Path("-*꣒=#\\🄣ﭘ𑴃\\ᤖ%3\u{1e00e}{YബL\'GE<|aȺ:\u{1daa8}𐮯{ಯ𝑠\\"), NixString(Smol("מ'%:")): Integer(-5866590459020557377), NixString(Heap("୪3\u{1a79}-l\u{bd7}Ξ<ন<")): Float(1.4001769654461214e-61), NixString(Heap("ங𞹔𑊍")): Path("*%q<%=LU.TჍw𭴟[Ѩ𑌭𞁈?Ì?X%מּ¥𞲥𞹒ౚ"), NixString(Heap("டѨמּ&'𐌺𫈔.=\u{5af}\u{10a39}₼\\G")): String(NixString(Smol("ₙ.\u{10f83}<x/Ⱥ𝼐$=𑵧{jE𞹺/f*𐢯ഒ𖿣﹂:ᨧ𐞴¥`%"))), NixString(Smol("𐠈𐠁`\u{b57}ꛪ`@$y")): Float(1.2295541964458574e-308), NixString(Smol("𐮫`:𐠂$4%\u{ac5}𐕯🕴")): String(NixString(Heap("='/𑍐<🕴\"=I᭻9𐮙\u{1e01b}"))), NixString(Heap("𑌏({?kG:ﺡ𑌵0𝒚q\"ঐ")): String(NixString(Heap(":ਕ"))), NixString(Heap("𑌐𞸡?𑙓,?🩒7\":'..'<𐀏𑓒Ეd𑊈ῠ𖽙3'&🀪𞅏")): Path("."), NixString(Smol("𑒬𑵡?KᲾWȺ`ᅯ0{<ኘzEÃ\"û𝤕$🞀𝋯�.\"𐕾\u{1daad}㈃\\𐤿ʊs")): Integer(2276487328720525493), NixString(Heap("𝔾\u{11c9f}<Δ]T𐞡𐞵𞹉�")): Bool(false), NixString(Heap("🕴Ⱥ\u{f82}/%*𛅐᠐Ѩ$🢕 *�)𝓁𑃀𝒦𐝂ö𑤕Ѩh")): Path("🕴")}))), String(NixString(Smol("&m𝈶%&+\u{ecd}:&¥\u{11d3d}°%'.𞹙2\u{10eac}-ඈ\u{11369}𞹰¥𒑴'xѨ𞹝.𑼖V"))), Path(""), Path("𑫒%J\u{11ca3}"), Integer(5535573838430566518), String(NixString(Heap("⿵𛅐🀰Z$,\\/v\\⏇\u{a02}ள𝕁?\u{11ef4}%&/�|\"<Ⱥ&cUÛἜᥣ𐠼𘴄"))), Float(-1.0028921870468647e243), Path(".j*𑣱ÜM𑈅I?MvZy:𐄂�𞹋%?Ⴧ%"), Null, String(NixString(Smol("ퟘ🕴𝒴𒐋$\u{afd}ਃÜѨ`\u{11d44}E\\;\"?𬛕$"))), String(NixString(Heap("/)!.P🇪ਾ'⮮𐰣א=tៜ⮏m:\u{1773}\"Ⴧsቪ+HNk"))), Float(-3.7874900882792316e-77), String(NixString(Heap("Ⱥ<𐺰L:🭝𐡆𞅀Ѩ𑬄a.m𐀼V\"𝋊A𝄀\u{1e131}﹝"))), Path("aᨩ�?"), Float(6.493959567338054e87), Null, Null, Float(1.13707995848205e-115), Integer(-4231403127163468251), Float(-0.0), Float(-1.1096242386070431e-45), Integer(-5080222544395825040), Integer(2218353666908906569), Bool(false), Bool(true), Null, Float(-334324631469448.56), String(NixString(Heap("j%ѨáѨਭ\"᠖𐔅𛲂"))), Null, Float(5.823165830825334e-224), Path("&𞹋𖭖:$\\𑂻&ኊ(𞹋LH{ꟓ@=\\nલ&lyໃd"), String(NixString(Smol(""))), Bool(false), Float(1.0892031562808535e81), Null, Integer(-3110788464663743166), Bool(false), Null, Null])), y: List(NixList([Integer(7749573686807634185), Float(0.0), Attrs(NixAttrs(Im({NixString(Heap("")): Null, NixString(Smol("*<Y")): Bool(true), NixString(Heap("*\u{eb5}*:꒶Ѩ&m🕴🛫:_\u{ecb}1$pk!\u{1183a}b*:")): String(NixString(Heap("🕴𞻰𛃁𞹛𱬒Ⱥ=*]\u{1bad}t\u{11d40}ං𞓤꧑\"Ⱥ\"\u{9d7}𬇨$/Û\"zz*ఏ"))), NixString(Smol("?�𐄀&TbY&<'ᾍ?ⶱ工=%¥Ѩ:<Ѩ")): Null, NixString(Smol("Y𐣵🢅$è𝕊f&5ꬢN🕴🉁z�𐺰Mꕁ")): Integer(7378053691404749008), NixString(Heap("]\u{1712}&🕴{𛲗భ")): Path("`ꬪᝋȺѨ𞹴:Ⱥꕎwp𖩂Ⴧ𑜶ল2/?¥\"DL¥?�\'𞹇𛅒p=Ⱥ"), NixString(Heap("`𝋤\"m\\.𐠼🕴𖺊?")): Null, NixString(Heap("\u{a71}ࡷ6lꙧ{�ன`&�GןQ$(")): String(NixString(Heap("f\"<\"X!"))), NixString(Heap("᪠cꪙ\\ਲࠐ🕴?Ⱥ\u{11c97}<🕴«Ù\u{10eff}𐒣:<íN%y\\𐮙ꩅR\"=e𐺭")): Float(0.0), NixString(Smol("𐮙MჍ:%ஊ\u{dd6}$Ù?,)Z/𑌉?Qo?=\u{b01}cȺ,\\*")): Integer(-750568219128686012), NixString(Smol("𖫵𑶨ಭ\u{10a06}\\C_=🕴\u{10eac}೨喝W]\u{fb3}Ⱥ8")): Path("vLeড়ys..Ѩ�𑈓!ଳ&y<ໂ§{EUⴭ꩗U*\u{1e132}"), NixString(Smol("𞢀TAC\\*2🕴>%Ѩ𑩮P?G\u{a0}f𮗻*v¥Uq\u{b62}")): Float(3.882048884582667e95), NixString(Smol("𞸱𐏍2$ml.?.*U𫊴꭪<~gへ𑾰")): Null, NixString(Smol("🕴%`᭄N{3?k𑓗%:/D𑤘ᅴP^9=Z៦")): Null, NixString(Smol("𫘦*ຢ'7﹠KѨj𝔊q\u{bc0}bલ𐤿%🕴�𞠳�L&¥")): Null}))), Path("A\u{a3c}l᪅q.ȺA&"), Integer(4907301083808036161), Bool(false), Null, Bool(true), Attrs(NixAttrs(Empty)), Bool(false), String(NixString(Smol("᧹H$T൧𞅏=𞹟🕴{[y"))), Path("\u{1da43}$�uῚÈ¥�¥\'\u{b82}ල\u{11d3a}Ⱥ:🆨`𝍨1%`=ꝵ\u{11d41}\\X:*ྈኋ𞸯"), Integer(-8507829082635299848), Integer(-3606086848261110558), Float(2.2176784412249313e-278), Bool(true), Float(-1.1853240079167073e253), Path("=𐠕ߕ*🕴ȺȺ:Ⱥ%L💇Ô\\`Ὑ%🢖ዀ ൿ\\🕴Iቫ=~;\"ȺΌ"), Null, Attrs(NixAttrs(Im({NixString(Heap("")): Float(-7.81298889375788e-309), NixString(Smol("%>=ꨜᇲກ𖭜\u{1a60}Q(/X㈌\"*{ㄟ`¨=&'")): Bool(true), NixString(Heap("%n𛄲Y'𞹧𞺧")): Bool(true), NixString(Smol("&/.ȺV�︒\u{afb}'𞹔~\\Oᬽ�")): Float(8.59582436625354e-309), NixString(Heap("&y")): Path(".<\'ꢿ.W&¥�"), NixString(Smol("'1\u{11d91}¥O-𑨩ȺrὝ¥:Wෳቍq𑼌@^𑊜?")): Bool(true), NixString(Smol("*E𑖬ꦊ¹𛅤ೡ/'𝐔j𐖏𞸹7)ㅚ")): Integer(3181119108337466410), NixString(Smol("*r¥[.ª\u{10a3a}\u{1e132}EB⵰𞹗")): String(NixString(Heap("{🟰𖫧🢚/𐍰'𞸁ൻ🃪�𛲃?s&2𑴉"))), NixString(Smol("*\u{1e08f}Sü🕴N'Eƪ")): Bool(false), NixString(Smol("/=`Ⱥ")): Float(-1.8155608059955555e-303), NixString(Heap("/੮RE=L,/*\"'𑊿=�+�🡨ᢒ𖮁ਹ𑱟Ѩk᧧\"\"R")): Integer(-1311391009295683341), NixString(Heap("?%$'<<-23᪕^ቝȺj\\𐴆")): Null, NixString(Heap("?&?⑁ವ\u{bd7}𑓘\u{112e9}ᝑl9p`")): Integer(-5524232499716374878), NixString(Smol("?״{j\"8ࢿ𞹔")): Integer(1965352054196388057), NixString(Smol("@𬖗{0.:?")): Null, NixString(Heap("C\"$𖮂\"/𐬗IyU_𑴈/N=\u{ac7}𐬉:ꬁ<ꟓ<.えG𖦐I/ᠤ")): Float(-2.6565358047869407e-299), NixString(Heap("G`🟡y𖾝𐣴`#+'<")): Float(-1.643412216185574e-73), NixString(Smol("O𞹤៷.?𒑲")): Integer(8639041881884941660), NixString(Smol("R<X𐚒¥=ጚ,.ᤦ/?{\u{b57}꯴ೝ/¥m🕴პ🟰f𒑱X_.")): Null, NixString(Heap("R珞એ�$f")): Null, NixString(Heap("ZbȺὖ")): Integer(300189569873494072), NixString(Smol("\\9r𝒢𐩈¥Ð𐼚\\?Ѩ{$")): Integer(-5531416284385072043), NixString(Smol("`j$�ⓧ\u{16b34}'ⷆt\\𞠏|🢒'%&𑂕𐖼/$\u{ac7}")): String(NixString(Heap("1=¡<𐞵១🢜Ð℧p\\4𐨗𐤿=ශ`.[<\u{dd6}."))), NixString(Smol("`ᥳ")): Bool(true), NixString(Heap("`𐨕``ቓ𑜿$*Dᤱ`:/}🕴N'𘅺ൎ7")): Path("/"), NixString(Heap("fෳ\\ßϐ*𞸭'%𑤁$jા=:Ѩt{\"0ߢ/ಐ𐁆𞹛i𗹱'🕴")): Bool(false), NixString(Smol("n6&<𞟤'JB¦x🩤vቚ\u{1e008}Ѩ¥𐖛j🕴b¥𐼙'\u{c00}a")): Path("\u{110c2}p:Ѩ\u{1a58}ⶰO<?𞸡𐖙"), NixString(Smol("ntU爵�^🪫'%&ਰ/")): Bool(false), NixString(Smol("o")): Path("¥�\'/ìRV𐄚"), NixString(Smol("t\u{2df0}b.𐠈¹𰃦*𖭞:🮻𛅤7𞢼𑊩XL\\ਐ.N")): Null, NixString(Heap("yg'«🡓")): Path("/`]𞻰`\u{1cf0c}Ë\u{c55}<bȺ�!"), NixString(Heap("y🕴𑤑/ල$🢱\\~\u{aaec}`ຄ")): String(NixString(Smol(":b🭑𑅟ౘත'�:hiⷊ*{*/ꙟ"))), NixString(Smol("{?^n𑴉🩻៵o<ಮz-뗨")): Bool(false), NixString(Smol("ð୭ண?𑅢\\<<%?<=$[<d𑋶\\w𐖔u<")): Attrs(NixAttrs(KV { name: Path("ଛ🕴R`𱡍=\u{2028}?¤𐔘ῐw41A𑃰𑥙<&:/"), value: Integer(-4007996343681736077) })), NixString(Smol("ù5 \u{c4a}ᝮ")): List(NixList([Integer(3829726756090354238), Bool(false), Integer(-7605748774039015772), Integer(-2904585304126015516), Float(1.668751782763388e125), Path("ጓ𑍐״𑖁$~෮¥"), Float(0.00010226480079820994), String(NixString(Heap("y¥C\u{c62}3"))), Integer(4954265069162436553), String(NixString(Heap("f𝔗/^%}₧Ѩ᪈\u{aa43}$𐆔𘴄𑤤N\u{c47}𑃹𑧄⿹𞹻"))), Bool(false), Float(7.883608538511117e36), Path("*ષඉ"), Integer(8893667840985960833), Null, String(NixString(Heap("Ѩ|f2\u{11300}Ⱥ\u{11374}🕴࿘\\e$ᢊR𑌐๔Ⴧ$�'`\u{1e028}🕴"))), Path("<Uc?𞹑\"🕴ᥳ:/ꬍ=ꮫ\\:ךּ&&jq\u{11d41}<_�%(῝Ⱥ�"), Float(-3.7947462255873233e189), Integer(1963485019377075037), Null, Integer(2642884952152033378), String(NixString(Heap("=\\Ѩqদ)%@�NH𑼄ⴭ.ዀ*Ⱥ$&\u{d01}`ভI𑩧h\u{1da9e}v𑀰/wl"))), Float(-3.1057935562909707e-153), Path("ÊȺ𐬗$?\'$ዀ%J`_𞹤"), String(NixString(Smol(":`'ຣ𐅔':𑴯\"R&r2h5\\�\\ਲ�<\u{11c3c}¥{Ⴭ!\":𝕆<*"))), Path("`%�Ⱥ࠽¥3Ⱥ?r&�🕴n<𐭰ȺȺᛞ$Ⱥ\u{a41}$ﭱප%\u{1b6b}.𖿰🛱?d"), String(NixString(Smol("ම𑌰d𞹺B𞹩&𑣄$ꬃO(ಝ{�/¥"))), Null, Path(""), Null, Integer(-5596806430051718833), String(NixString(Heap("Ѩ.<\\?𑴠¥<ப=~湮𑤉`v\\Hf\u{ac5}Lᾑ&.𞥟🕴5A\\'¥"))), Integer(-4563843222966965028), Integer(-1260016740228553697), Path("𑼊W𐄡ຄ<u\u{11357}e"), Float(-1.4738886746660203e-287), Float(-2.1710863308744702e271), Integer(-4463138123798208283), Null, Integer(7334938770111854006), String(NixString(Smol("&<\\𖺎CKཛ="))), Null, Float(3.6654773986616826e238), Path("¥\"=ᝯ𐢭$ஏ{.𐢫8ujx"), Bool(true), Path("𐩼ᨅ�\\ம}oѨ𐖌Y$�/z𑇧/`%¼𖭘𑃦:ᥲ$-"), Integer(4610198342416185998), Integer(-8760902751118060791), Path("Hí{𖬩~\u{733}{𝒹\':𞅀ݼ:𑣘Aஜp𑦧𞁦K=Z*"), String(NixString(Smol("\\\"\\O=𝆩𐝋0🕴\">.🟇/𝔇`¥ⷒ"))), String(NixString(Smol(".𝼩𑵢"))), Path("&cἾV&🈫WȺ2{:Uஔi𢢨�$\\Ѩⷉ<+𞥑︾�🢅𝄦"), Bool(false), String(NixString(Smol("?H\"ᝨᛧD🕴e"))), Bool(false), Null, Path("~"), String(NixString(Heap("*<𐄘బu;.𝁮🛵𞸹g\\mF%[LgG.𐭸𐫃*倱🕴`"))), String(NixString(Smol("`ó¥!0ѨW.ଠಏퟞ\\ਫ਼?🫳"))), Integer(5647514456840216227), Null, Bool(true), Bool(false), Integer(-7154144835313791397), Path("\\=🕴�ᣲ*𞹷𛲕cꮈ🫣CȺÏ𑤉נּ/$ਜ.\u{dd6}*%আ`𐄿y꡶"), String(NixString(Smol("`2¥/�ꫤX\"Lᱽ"))), Float(-1.5486826515105273e-100), Bool(false), Path("\\A6𝼥^]<🢖"), Null, Path("G`6𱡎%\u{1e08f}ᳰ"), Float(0.0), Float(-5.1289125333715925e299), Integer(-2181421333849729760), Bool(false), Null, Float(1.8473914799193903e206), Float(-0.0), Integer(-1376655844349042067), Integer(-5430097094598507290)])), NixString(Heap("Ϳ¥%h:?=$🟙p\u{1cf24}*𑴠Ⱥ]Xb")): Path("]l\'*𑇡\u{1e08f}*𝄍&,÷nc෴G¥,🕴𑌏+`?"), NixString(Smol("\u{85b}/")): String(NixString(Smol("%Ⱥ{𑤉pO𑱀$d/ñPF\"="))), NixString(Smol("\u{a51}H𞹾$`𐒡:�¥𐝡{𐺙౾�i${ಇTG��¥{`Òބ^")): Path("<𛁤Eh𑱅"), NixString(Heap("ક\"1ਐȺ")): String(NixString(Smol("𞹉ÕI$𑁔﹫xpnὝ{`RgX.&]ଘ"))), NixString(Smol("ங+ח𐼌ஏ:=R0D\u{afe}ð<%𖭴?/CT%Ⱥo=?𞥔𐴷\"")): Null, NixString(Heap("\u{e4d}/Ð\u{11d3c}m6౨࿒ਫ਼K¥u\\𝐪ୋ")): String(NixString(Smol("𐋴+Z\\𞸻kמּﲿ𐤨tn>ΐ/>3Ѩ<E{𐧆!"))), NixString(Heap("ጒ.ퟴ%\u{1e016}🕴𑨍ἢ=~\\:7𐦜")): Integer(7492187363855822507), NixString(Smol("ⱅ/𞹯=\\ꬆ^ᰢ.F𒿤𒾖ȺlȺÐ")): String(NixString(Smol("𞹗R"))), NixString(Heap("ⶤB2.$\u{10a05}𖫦&*\";y$¸𛲒𑊲U🕴Û\\🕴𞹂E಄,୨")): Float(4.8766603500240926e-73), NixString(Smol("ꬮ\\'\"A\\\\\"R.")): Bool(false), NixString(Heap("漢$%`Ⱥ")): Float(-6.1502027459326004e57), NixString(Heap("ᅵ𛲅</\"\\:�=h𑵥V=\u{c4a}")): Path(""), NixString(Heap("�I𐔋\u{1bc9d}\u{1e029}𛅹𑻨𐊾I¥?ѨѨ:�È\\'𞟾'Ѩ")): Float(4.528490506607037e180), NixString(Heap("𐮪qՎ4𝒦?F𐙍?")): String(NixString(Heap("`🂻𐺭` 𑊚ൽ/ⶻ🛶fȺ(f𐖻Έ{᪐𑌫Z%𑍍ꦘ𐴐&zdৡ𑼩"))), NixString(Heap("𒓸ං#.Ѩ㈞�i")): Path("$/"), NixString(Smol("𖤔𐖔%𖭖\u{1bc9e}")): Float(-2.70849802708656e-257), NixString(Heap("𖾖:y'𐮫dmvਫ਼`*QR𐏐::P=\\B🕴\"c𒓕eᎾΑ")): Path("\\ÔA"), NixString(Heap("𞄐'𖭕:𖽭𐖘\"{Ⱥ.\"⺈??ৎ🁡ଊ𦩽🕴𞣏w.:ࡰ")): Null, NixString(Heap("𞺖Ⱥೇ𥢁⹂Ѩ@ઋ𞹏<Ⱥ6ⴏ𑽗ஶ=L𑍐M.<ꭚ*J\"@~𝁌$\\]")): Float(-2.5300443460528325e91), NixString(Smol("🕴7𪻎𑃦𑤏+ῴᎣ\\ౝ?ட\"\\ꜭ")): Integer(8622149561196801422)}))), Path("I<:🫢:.𑋛ோ\'ⶣ[𑆔%)எM!1<ூ-J>/`$🠁<\\u*ল"), String(NixString(Smol("M|s?ଏ\\"))), Float(-0.0), Integer(6467180586052157790), Bool(false), Bool(true), Float(8.564068787661153e-156), Float(6.773183212257874e294), Integer(4333417029772452811), List(NixList([String(NixString(Smol("/%\u{9d7}𖮎\u{b43}𑰅𝋓ᝠi`\u{a02}aѨ𐢒>ⴭȺ𑌃C፧Â𐭰>G\"ፙ𐍁𐠈&$"))), Integer(2000343086436224127), Integer(3499236969186180442), Integer(4699855887288445431), String(NixString(Heap("ங𐮛𖿣Y𘡌`.𒑳𞋤R7$@`")))])), Bool(false), String(NixString(Heap("*🕴,/𐀬tyk𒑰\u{f90}"))), Integer(5929691397747217334), String(NixString(Smol(".=𝒢.Eⶇ⁃੮\u{fe04}𛅕C៰🢝Ὑ`{.g¥¥"))), Path("\\*J(\'%\u{1a68}k\':ⷋ?/%&"), Bool(false), Float(3.7904416693932316e-70), String(NixString(Heap("/Ⱥc$𐠼<�⾹ഉ "))), Integer(3823980300672166035), Null, Null, Bool(true), String(NixString(Heap("$�𐖔aᅨවw-=$🕴$𞹟xѨb🫂,mຄ"))), Float(-5.5969604383718855e-279), Path("Ἓቇ !\'𐍈𑙥&ಐz"), Bool(true), Integer(429169896063360948), Float(8.239424415661606e-193), Path(""), Attrs(NixAttrs(KV { name: Null, value: Float(-3.5244218644363005e64) })), Float(2.1261149106688998e-250), Float(2322171.9185311636), Integer(5934552133431813912), Integer(5774025761810842546), Float(7.97420158066399e225), Integer(4350620466621982631), Attrs(NixAttrs(Empty)), Integer(-6698369106426730093), Bool(false), Null, Null, Float(-5.41368837946135e190), Null, Path("\u{1112b}`¥𐀇=h𛅕`/?qG%GȺ\u{cd5}𝔼.𞊠\'\'."), Null, Bool(true), Float(-1.0226054851755721e-231), String(NixString(Heap("\"$ල%𐴴*s\"D:ᘯᜩ9𑌗"))), Integer(-713882901472215672), Path("/{𝇘𑒥*ﬧH`ਸ਼í$𞲏ῄZ`🫳𓊯vg]YசȺS𞹢𑼱ó3")])) }
+cc b0bf56ae751ef47cd6a2fc751b278f1246d61497fbf3f7235fe586d830df8ebd # shrinks to input = _TransitiveArgs { x: Attrs(NixAttrs(Im({NixString(Heap("")): Bool(false), NixString(Heap("\"𑃷{+🕴𐹷𐌉𞥞'🯃ⶢvPw")): Bool(false), NixString(Heap("#z1j B\u{9d7}BUQÉ\"𞹎%-𑵣Შ")): Path("𐆠az𐮯\u{c56}�ㄘ&𞄕ନz[zdஐ�%𐕛*ȺDዻ{𞊭꠱:."), NixString(Smol("&?")): Float(-1.47353330827237e-166), NixString(Smol("&🡹B$ѨK-/<4JvѨȺn?\u{11369}𐠈D-%େ/=ਹ𛂔")): Float(-1.2013756386823606e-129), NixString(Heap("'")): Float(-0.0), NixString(Heap(".`%+ᩣ\"HÎ&\"𐊸A%ἚO🅂<𞺦¥ைEAh.⥘?𑩦")): Integer(-5195668090573806811), NixString(Heap("4l\"𖫭¥r&𐡈{\u{11c9b}&磌લ𐀷𑊣Â\"'𓄋{`?¬𝔔")): Attrs(NixAttrs(Im({NixString(Heap("\"!`=Wj㆐ᬢ\\è\u{1183a}T\u{11046}ꬕ&ȺȺ")): Float(-8.138682035627315e228), NixString(Smol("$E𑱼ஃ:\"ெB🮮.�🡾🕴:𑍇𛲜")): String(NixString(Smol("Qf1𝜻A\\L'?U"))), NixString(Heap("$kส𐀽ச𐠸<`\u{f37}5ቘ\u{cc6}G\"1ລÕ𑙓ἡמּt/𖭒𝓨₃ÑѨ𑤨a𖿰ѨA")): Integer(-8744311468850207194), NixString(Smol("%'?Xl(ࡻ.C+T𝒿𘴈L-𑌳\\\u{1cd1}bK>SA")): Path("\\🕴એѨ𐹼-﹔Ð𘴂:?{\u{1e02a}¥\u{c46}#𚿾?K"), NixString(Heap("%e🕴v𐢕&:שׂத")): String(NixString(Heap(""))), NixString(Smol("%¥\u{1d167} |M")): String(NixString(Heap("𒌶$O🁹/𑑝ò"))), NixString(Heap("&=?\u{ec9}Ⱥuい𑜿Ѩ/�𝼦\"$𑤷T{?ⶾ,𑋲9Ⱥ�")): Null, NixString(Smol("&𑌖𐎀?`F𑍍g¥`\"Ⱥ𖿢ລ𝼦L{\u{b01}ѨO*.&K%🫳\"🕴f𑆓¥/")): Null, NixString(Smol("'P𖮆𐽀𐓲𝔊SⶄᰎE\\kF𑦣`�ఎG&/K*")): Path("ᠦlѨꬮ𞸷:\u{1344f}ዀME.&\u{aff}𐧣ᡋb𐦟ቘᄨ"), NixString(Heap("'౻🕴_𐠷𑍡!')?&🕴.\\{{𐾵�*>sj\u{9e3}న`Ѩ¤")): Bool(true), NixString(Smol("+ས&1:᳇P%{r?Ѩ/d`𐠷\u{1ac5}>'🢧")): Bool(true), NixString(Smol("3\\Ѩ龜=&පౝ𑈋🢱\"/<.&🕴/ૠᥬ ̄|&Â$'")): Bool(false), NixString(Heap("6|ዅ`ժy𐠃*")): Bool(false), NixString(Smol("8=᠊<ඩTRળ(Q𑝆=෨\\?Ϳ{>n&")): Null, NixString(Heap(":6zꬑ\\फ़\u{1cf15}װ𝼦🛩Lv<?\u{10a3a}*𑌂wೝ𝓃u%C.R%$෪l")): Bool(true), NixString(Smol("<V🛷VȺ𐩈𓈤ౝ&ₔѨ")): Integer(-2521102349508766000), NixString(Heap("=/e�S=d𐖪\\bᘔ8$Z'")): Float(1.7754451890487876e-308), NixString(Smol("D𑊓Ꮅ%")): String(NixString(Heap("𑌏ë;ල"))), NixString(Heap("E</")): Float(1.74088780910557e-309), NixString(Heap("Fༀ.൷𒀖>¹𐍮Ⱥ$M\\\u{10a3f}ಎ𒿮Ⴭ5ዓ6{🕴:")): Null, NixString(Heap("GCક\\¥{4🕴?ࠏ🕴=🕴𖭝'?𐝥%Ⱥ\u{10f48}�kኲ:%¥")): Path("𝉅w𞹟`ῼ`Ѩh/\u{11d3d}\u{65e}j/🉥\\&𛅥אּѨ𑧒\"Ѩkໄ\\a~𑚩-(:"), NixString(Smol("K|*`\u{9e2}Ѩ𖠉%𑦺=u</ৌ⮕ꬋ\u{a01}*6𐩒P᰿𞅎'🉁")): Bool(false), NixString(Smol("MSᅳ")): Integer(-8585013260819116073), NixString(Smol("P&ꩁ࡞🩠&𑈳:⮲.ز")): String(NixString(Heap("G]Ѩ{D"))), NixString(Heap("R𝒞j:𝄁{𑍝ﹰ\u{309a}5Ѩyべk🟰𑊈𱁿:𑑡'🉥ⷊ{ౠ<=&:ᦗ*_K")): Float(1.215573811363203e25), NixString(Heap("Xoy_Z🕴𞅈ⷈ⁵\"ݭ)<𑽎]𒒴")): Float(-2.0196288842285875e215), NixString(Heap("[y𞹨/")): Path("(p\'/.Ⱥࡹ?🄎ቌm�>>%z~{`%4ѨȺ𑫎"), NixString(Smol("[𝒻W𑬀\")a𛄲𑰄&ⶹ.\":𖮂")): Path("\u{a51}<:ꫧ㈘🕴𑒻gȺ𑌃D🮦𫝓"), NixString(Heap("^?𔕃{\"ꢭ𝍥{💿o𝼨\u{b01}Ѩõ")): Bool(true), NixString(Smol("`.\u{1e005}&Ⱥ🕴=%𑩦৩\u{a3c}{ⷜ:F�h'\":Ὓ࠸")): Integer(958752561685496671), NixString(Smol("`𑤓(b𝔵3=𞁕<\u{f93}𐇞𖭶$🕴¥.:&?=oఋN\u{9c3}")): Float(-1.2016436878109123e-90), NixString(Heap("`🂣`𐤢.üI¥::.$)𐨵/\\𒑚ZὍ𖹕e𝒟具Yຈ|🀙࡞")): Path("/ᅵȺ𝒽Ⴭ<🉀u)🕴מּ/ந"), NixString(Smol("d®댓7M𑍐_‽sxⶋ𖩈Xⶭ]ⷓ?`o𞥟\"@,ㄛ𞲔ೠ🕴j#y{'")): Path("ቝg𑅇ዀ🕴𐌼�t=:.|*]�🕴,Ѩ*ᝡㄧ¥nȺ"), NixString(Heap("eቊഎȺﹰ¾0𐣰/அ🠄r~=𐞌_ఽ")): Integer(-4346221131161118847), NixString(Smol("e𐠪ቘ<$=Q\u{ecc}𚿷*}ଐ$/𐂢Y?y\u{11d3d}fଐ𐋹\u{20ed}H\\�û'g<L{")): Integer(1302899345904266282), NixString(Heap("fJ🉀!6$F𑰤𐎁C&ౚ𞓤T\"\u{d81}«Տ𘴃.𑰍Â﬩*")): Float(-9.550597053049143e239), NixString(Heap("j﮼\"🕴$%'Gૌ3?\u{302d}¥")): Bool(false), NixString(Heap("u~Ⱥ¥𝣤YN𐂜 ¥' `r𐿄/\u{b57}")): Integer(3809742241627978303), NixString(Smol("z%c流ü🕴$`.F*`Ѩଏ<\"𣅅'<3p=r:Y\u{1a60}ড:/]𐾁")): String(NixString(Smol("\\*E■{:🕴"))), NixString(Heap("{<B9-Y𑣊N61ѨH.¡\\ꮬ'Ãkô")): Path("=𑥘^*¥K𚿾સ*Ðᝰf[:P𐤅*<⻡<\"8"), NixString(Smol("{ຄ±𑼉Ѩ`ⶮ𚿺Í%𞹡`!/")): Integer(-7455997128197210401), NixString(Smol("|🕴ේ0\u{1e131}🡑=ਜ਼_`𝔗.ો1ㄈ𐹣\u{1773}ꭆⵯ\u{1c2e}ࠨ𐧔੫=\u{b42}1\u{bd7}�/�¥$")): Null, NixString(Heap("¥⵰ᅬ<*'Z¥\"Ὓ$ᥖᅭ𚿱\"ꥳÒ𖿱𞓕Ѩ9MÌ¥u{ୈ<﹨")): Float(6.4374351060383694e243), NixString(Smol("¥🕴=r`z&.<V<.Ⱥ๏\"")): Bool(true), NixString(Smol("Ê9𝐐/JkᡶUl\"🕴{i")): Path("z/¥&Wzꡭ`?Ѩ\\🟰þ၈$ࠓ"), NixString(Heap("Ù𑥘c\"�\u{8ce}𑤸𝆥೮?~")): Bool(true), NixString(Heap("ãU&𐔐/")): Bool(false), NixString(Heap("ѨѨ?<'?࿎ȺՉⷊ$\u{fe09}ꞙ")): String(NixString(Smol("Ό/`j🕴🕴𐞂🕴\u{ce3}ਲ$𘟛ºװ?𞸮J𝍣Q\"}🕴q𐧸ࢺ\u{a48}"))), NixString(Heap("ࡩ꯳%^ౝ")): String(NixString(Smol("{%"))), NixString(Heap("\u{b57}🪃ⷚȺMC?¥\u{11c9a}+:*<B𐖰y~/.1&$𐺭hর\"\"")): Path("ቕ𝌫=(|R{ei\u{1a5b}Z!2\u{c3c}+<rí:\"f"), NixString(Heap("ங¸4ᨔ\u{17cb}ਜ'ಣ¥z&=='c𜽫ࡠﹳ꠲𐨖এ<¥🕴𑙫𐖷")): Integer(-2404377207820357643), NixString(Heap("ቘ🕴{ȺL'.")): Null, NixString(Smol("ኸ\"𖦆d\\&𐞷C🠀k1\u{eb1}KV%🪿\u{1cf35}>:3𐡾P.\"Ѩ.ໆኍ*'")): Bool(true), NixString(Heap("ኸ8")): String(NixString(Smol("�?Ia🀛*\u{10d27}U¥𐖙9ીㇺIW:%&G+?"))), NixString(Heap("ᱰῙ:ஙᤄ.:*𐺰𝄿勉Ѩ&¥𐧾7&`AຂѨ𑗒&¥𛅕/🕴Ѩ")): Float(-0.0), NixString(Heap("ꝏs𞊡<g=𝕃ᅭRȺ�u𛱜")): Bool(false), NixString(Heap("ꡩ$𰻍🯷@a\\")): Null, NixString(Smol("𐭻𞸧Ã\\Ⱥ𞸢𘌧%h+|@ꖦ*\"~𑄣")): Bool(false), NixString(Smol("𑐑𞟣Ѩ<''¥Ѩ<ອ᪦\"፵𛄲𞀾ឮo")): Float(1.540295382708916e-173), NixString(Heap("\u{1145e}p\u{ac1}e\u{10a05}𝼩#ந*/?🡗\u{c4d}ͼᲿꟑ")): Integer(-4194082747744625061), NixString(Smol("𑘏𞟨:Ѩ?^ c🃊tๆ.Ⱥ9 𔑳&៵ׯ🬔\u{11f01}𬢪*ﷲȺ`𑼆n")): Path("7�$Ì\u{1e00c}海2&𐝀<"), NixString(Smol("𑚆")): String(NixString(Smol("𝒢\u{11727};க\u{c56}P'𝄎🪬*ⷍ\u{1e08f}59/𑨙𐤿ොZ$Jෳ*𞺓"))), NixString(Heap("𑴋¥#=%")): Integer(1639298533614063138), NixString(Heap("\u{11d3d}𑊌o:}=ਫ਼$໓ᅨѨ𑌅6🫄ᅮ🛴")): Integer(4745566200697725742), NixString(Heap("\u{16af4}𞲡🡕:ೈ𝒩$𝄀\u{c3e}ࡨ")): Float(2.8652739787522095e-21), NixString(Heap("𖽩$,®'w5ޤ*𖿣H'6¥ੀ")): Path("+ꡕ:/f"), NixString(Heap("𞋿@%\u{20d6}÷MqȺÏ🪧𞟣&𝼆Ⱥn2?ᦽ")): Path("n𞺣Ꮝ*𐤤FU.T"), NixString(Smol("🕴/Ⱥᜄ&{.\u{e0109}]𐣨|ຆ᱓𐃯🂥𐺭𑤬@$r{Ⱥ‿🕴〧\u{c3e}'X")): Null, NixString(Smol("🕴<V𖬑u\u{bd7}ໆC`xῴ𐄂f.M^ਐ𑼂;%.𐠈¥໔ꘄዀѨઑ")): Null, NixString(Heap("🕴¥\u{10a38}/{\\-?🕴'Ýὓ𛱱`XuᾺ𐊎ࠅ|𞹒𐆗𑌲")): Bool(true)}))), NixString(Heap("9;G'.%𑴆")): String(NixString(Heap("🕴8?$=𞺒%V*\u{9bc}𑊄ଡ଼JbѨ=ળ=𑛉\u{a81}É;ᅤᲿN=ਫ਼:�`𐁙"))), NixString(Smol(":Ѩ\\Lꝅ5𐤀'uꦙ~*肋J")): List(NixList([String(NixString(Smol(".࡞\\P?*<<\"𐖄9&𘚯%Ѩю1G\\౭𒑃&𞹛=9:𞥐@("))), Path("<𝒗ঽ¥<\u{fa7}ୌD={=ଡ଼\"Ⱥ\u{bd7}ઐ$\'�¥𞹼E𐦙🡓t`\u{11357}"), Float(-3.099020840259708e-255), Path("𞟫᪓h𖭁")])), NixString(Heap("<:^X𞹇𞹷\\\u{f7e}\u{1713}^ⴝQ`.|G𝂜𞹍𑐬𐮅{x𑍇סּ\"`🉤𝚚𰳳Ⱥ")): Integer(6011461020988750685), NixString(Smol("<{פּ%#¥🕴d:�,y")): Float(-2.901654271651391e201), NixString(Heap("=/I\"🮧{z&a�<?`L¥𞋿``.|")): String(NixString(Smol("=𖫉m\"ȺhC_"))), NixString(Smol("=L=X'aਿ9.")): Path("<&]Mভ"), NixString(Smol("Eנּ:b$?JὙs𝒢O?ꥠѨ{\\ਸ𝕎𒾮`𐀢")): Float(3.6870021161947895e-16), NixString(Heap("\\�' ìrᩆ#ລB¥𐖻l%𛅥D3{/.4O¥\\.{𐼓^𒾬")): Path("/íR›d🕴-K\u{ac8}"), NixString(Heap("\\\u{1e028}೫I/ͽ4")): Float(-8.628193252502098e81), NixString(Heap("a𐠈/K33/&𝐵¥𐼰.y/🫢\\𐧨𞟹")): String(NixString(Heap("𞹡W<᱂a=ᡞಇ�%&𑴈ࢳ¥𑊈"))), NixString(Smol("eD�¥𐌋x<`B𞣍H\\`èZ𐐅៥{\\?T*")): Bool(false), NixString(Heap("oP𝒬(ѨJਈ𛀺Ѩ\"?T=O𛄲s�🕴LK🝈ከⶩ+\u{1344f}hu?E")): Bool(false), NixString(Heap("vNꟓȺM]")): Float(-2.9637346989743125e232), NixString(Smol("{!¥\\\u{a47}\u{10a38}.E4Ѩ;=R\u{a48}<=/\\&స𐢩N'?.9𑗊\u{7ec}ਸ਼:𐫃")): Null, NixString(Smol("~+!$M.𑐯ౚ\\>.¥𐖥<Q'໙0.kQ{𞹾༺𑼉sEὕ")): Float(9.45765137266644e-294), NixString(Smol("¥;Z$*&+�&\"🉆`ኲ")): Null, NixString(Heap("ãF&<` \u{64c}-:Ѩ*�")): Integer(-7654340132753689736), NixString(Heap("Ⱥ`\u{a81}Ⱥ𞺥\u{11727}<m\u{a51}{$`\u{11d3d}શR/E")): Bool(true), NixString(Heap("Ѩ%gὋ 4𑶠𑤕Y{Q<")): String(NixString(Heap("𛃛Ꟑꩀ𑊈RoR<ৌ:4`ⶥ(ೳ>8*𑌟/𛅧<}&꒩᠙\""))), NixString(Heap("ѨನQ𑠃%=i𘓦ශ\u{1939}𞟭7;ﺸ𐼗ꟘW")): Bool(true), NixString(Smol("ਬͿ𝒩./🪡Ѩ𝁾ໆ+ఫJ{𑰏*\u{cd6}ΐѨ'APȺ*")): Path("Q%︹֏Ὓ<$Ꟗ$ᦣ𐖻Ѩ\u{c3c}MѨ"), NixString(Heap("ଢ଼")): List(NixList([Path("?&�=\u{fe0a}z\"વ🟰}ৱᦥ𐕅\"𞹭MⷝDJຄ"), String(NixString(Smol("𐺱`𞱸Ⱥ🜛?ୈ&Ꞣ=j¥`൷@ਐෂ't=7)ꬖన2Dꪩl𐝣ጓ=W"))), Path("`ಲ=𑘀eલ7𖽧Ù&ᜉ:.ﺴ𐳂\'𞹎n🕴𓊚>𝓓ை:\'{`o"), Null, Bool(true), Float(-1.424807473222878e-261), String(NixString(Heap("𑤅w𑌏ኸ¡ਊ/𞺩Ⱥ\\𛅤𞢊?\""))), Null, Bool(true), Float(-4.714901519361897e-299), Null, Integer(2676153683650725840), Null, Integer(3879649205909941200), Bool(false), Integer(-7874695792262285476), String(NixString(Heap("Wᥳ⑁*\\%ᜦ.⮏꫞Q𞹔L|ௌѨdU=*Ѩ\u{20dc}"))), Null, String(NixString(Smol("U𑐎𖺗$𞹡𞅏𝔯﮴<)&𑌫\\{\u{1acd}wѨ!𞸤𖭓º"))), Integer(802198132362652319), Path(":\u{b4d}Fᝊ:w:꯵"), Integer(-8241314039419932440), Null, Bool(true), Float(-0.0), Float(0.0), Integer(-3815417798906879402), Path("\"Dෆૌ෪\u{b01}ૐ%M№"), Null, Path("ab`𐒨\\{÷௶𖮌$=ë"), Bool(true), Null, Float(2.607265033718189e-240), String(NixString(Smol("𐖤𑤷=6$`:0ѨE\"Ѩ\"𗾮ኴ}.𞠸𞺢ߔ𑵡"))), Integer(340535348291582986), String(NixString(Heap("᠑%*&t𐮮':\\\"ꩂລ'\"𞟳|J\\\\V𑧈𛇝췭𐮬$\u{eb4}"))), Null, Float(-0.0), Float(2.533509218227985e-50), Integer(2424692299527350019), Integer(8550372276678005182), Integer(2463774675297034756), Float(-1.5273858905127126e203), String(NixString(Smol("𐕐𐐀ùI"))), String(NixString(Heap("?:🕴𑴞ﹲѨË𐫬Zf{𝋍{{࿀&\"Ô:<CJN?"))), Integer(-916756719790576181), Float(5.300552697992164e116), Path("𞹝�.?𝌼uଢ଼%~"), Bool(true), Float(-4.423451855615858e107), String(NixString(Smol("Ώﮱ¥8gH^ᛨꟖjR𐢮\"S𖩑𞸻🪁"))), Integer(8503745651802746605), Integer(8360793923494146338), Bool(false), Path("𝄞3¨/𑢸mº~𑍋ha𐫮$⹎\u{dd6}*ᲬѨク?𝔔¿"), Null, Float(-1.116670032902463e-188)])), NixString(Smol("ഌ⾞V𑄾7𑚅D𞅎$\"")): String(NixString(Heap("¤<ꡀὝ¥𞸤𞸇𑴄�ⷍa𑁟O\"f&𘴇`*<z𑨧ᤗ"))), NixString(Smol("ዄS𐾆ꟓ\")ኛ7G<oѨ🕴?𛄲×𑿕ᩣb。𖩒w")): Float(-2.1782297015402654e-308), NixString(Heap("ᩌਲ਼Ⱥﹲ<'=ւລמּ�p")): Path("𐀽𐠄"), NixString(Smol("\u{1a7b}$༄XୈÂ`7\"$*=࡞៣𐀸.tw")): String(NixString(Smol("ﬖ<j%𑊊kૐmȺrᎇ𒄹ëল.y*ᤫCョB½"))), NixString(Smol("Ὸ:𝍷🢰8g>\u{fe0d}RѨꥫ^ῷ&ຈ/'𑌅gው=s𝂀'�𑛂𞴍~𑅮?𝑪")): Float(1.6480784640549027e-202), NixString(Smol("ⴭ8🕴V𞹙2𑶦ං=")): String(NixString(Heap("ਓ𞹗Ѩ𖭨𝓼¥..ᾶ`oU{ₖB\\©:/ѨI𝆍.&🕴:ම𞹉fi*v"))), NixString(Smol("יּN\"@𐦽Ë`𑇑ȺC{¥b$ಭೱp$hS[좵&")): Float(0.0), NixString(Smol("ﺵ🟰\"nj𞺡o&*Oz{Ⱥ\"/\\፨\u{8dd}^v´𞹺`")): Path("𑇞.🕂d"), NixString(Smol("�𞸹מּz?_R'ᥲ<<¥/*﹛Ⱥ�R𑗓𑙥𑜄ೱa'𐀞7")): Float(1.6499558125107454e233), NixString(Heap("𐏊എ🟩ﬔV?Ѩ𝔢𖭡\u{1bc9d}𝌅𑤸ڽ𞹺V.𝕊B⸛*")): Null, NixString(Heap("𐞢u1%<🕴â}𛱷$٭b𛲟ຜv着\"𝕃\\$Ⱥ<ºףּ%ଓ")): Null, NixString(Heap("𐞶𐏃*$🂭eස\u{dd6};𞁝'ᩯ")): String(NixString(Heap("'&ⷘᰎ\"𐠸𑈪"))), NixString(Heap("\u{10a05}/")): String(NixString(Heap("ㄴኲY¥'𑛅Ῥ\"𔕑𐖕נּ🇽$l.=$🠤𛰜-𝒢R{$'$^"))), NixString(Heap("\u{11300}🃆+$�ₜj\"ჇA*r&𑥐𑊙W\u{10a0d}yR<\\Ⱥ׳𞄽C&?")): Integer(6886651566886381061), NixString(Heap("𑍌🕴`'\\G¥<ꬤⶮM%𑵠꒦ૉS\u{9d7}Ⴭඹ8🕴*[Ö")): Float(2.2707656538220278e250), NixString(Smol("𘴀'>$¦C/`M*𞻰¢꩙᠖�*🕴:&è")): String(NixString(Smol("'<ড়ᰦ"))), NixString(Smol("𞊭")): Attrs(NixAttrs(Empty)), NixString(Heap("𞥖🢙=ཏ=�%𛲁$zi¥&꧗𫝒૱G𐧠𝓾$/𐼍𫟨Yhⷕ")): String(NixString(Smol("Us:)?𘘼"))), NixString(Smol("𞹪*𑌳`f𝐋ଐ𝕆j")): String(NixString(Heap("𖭿5^Zq𐨗𑄿𑥄ߤ𑜱2ঐ"))), NixString(Smol("𱾼\u{1e4ef}õ<\u{a0}{ö\\$�ῷq")): Null}))), y: Attrs(NixAttrs(KV { name: String(NixString(Smol("�𛲑ો\\?É'{<?W2𫳫p%"))), value: Integer(134481456438872098) })), z: Attrs(NixAttrs(Empty)) }
diff --git a/tvix/eval/src/builtins/impure.rs b/tvix/eval/src/builtins/impure.rs
new file mode 100644
index 000000000000..adf43e1a2ea9
--- /dev/null
+++ b/tvix/eval/src/builtins/impure.rs
@@ -0,0 +1,81 @@
+use builtin_macros::builtins;
+use genawaiter::rc::Gen;
+
+use std::{
+    env,
+    time::{SystemTime, UNIX_EPOCH},
+};
+
+use crate::{
+    errors::ErrorKind,
+    io::FileType,
+    value::NixAttrs,
+    vm::generators::{self, GenCo},
+    NixString, Value,
+};
+
+#[builtins]
+mod impure_builtins {
+    use super::*;
+    use crate::builtins::coerce_value_to_path;
+
+    #[builtin("getEnv")]
+    async fn builtin_get_env(co: GenCo, var: Value) -> Result<Value, ErrorKind> {
+        Ok(env::var(var.to_str()?).unwrap_or_else(|_| "".into()).into())
+    }
+
+    #[builtin("pathExists")]
+    async fn builtin_path_exists(co: GenCo, path: Value) -> Result<Value, ErrorKind> {
+        let path = coerce_value_to_path(&co, path).await?;
+        Ok(generators::request_path_exists(&co, path).await)
+    }
+
+    #[builtin("readDir")]
+    async fn builtin_read_dir(co: GenCo, path: Value) -> Result<Value, ErrorKind> {
+        let path = coerce_value_to_path(&co, path).await?;
+
+        let dir = generators::request_read_dir(&co, path).await;
+        let res = dir.into_iter().map(|(name, ftype)| {
+            (
+                NixString::from(name.as_str()),
+                Value::String(
+                    match ftype {
+                        FileType::Directory => "directory",
+                        FileType::Regular => "regular",
+                        FileType::Symlink => "symlink",
+                        FileType::Unknown => "unknown",
+                    }
+                    .into(),
+                ),
+            )
+        });
+
+        Ok(Value::attrs(NixAttrs::from_iter(res)))
+    }
+
+    #[builtin("readFile")]
+    async fn builtin_read_file(co: GenCo, path: Value) -> Result<Value, ErrorKind> {
+        let path = coerce_value_to_path(&co, path).await?;
+        Ok(generators::request_read_to_string(&co, path).await)
+    }
+}
+
+/// Return all impure builtins, that is all builtins which may perform I/O
+/// outside of the VM and so cannot be used in all contexts (e.g. WASM).
+pub fn impure_builtins() -> Vec<(&'static str, Value)> {
+    let mut result = impure_builtins::builtins();
+
+    // currentTime pins the time at which evaluation was started
+    {
+        let seconds = match SystemTime::now().duration_since(UNIX_EPOCH) {
+            Ok(dur) => dur.as_secs() as i64,
+
+            // This case is hit if the system time is *before* epoch.
+            Err(err) => -(err.duration().as_secs() as i64),
+        };
+
+        result.push(("currentTime", Value::Integer(seconds)));
+    }
+
+    result
+}
diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs
new file mode 100644
index 000000000000..53ad6f3f8e50
--- /dev/null
+++ b/tvix/eval/src/builtins/mod.rs
@@ -0,0 +1,1037 @@
+//! This module implements the builtins exposed in the Nix language.
+//!
+//! See //tvix/eval/docs/builtins.md for a some context on the
+//! available builtins in Nix.
+
+use builtin_macros::builtins;
+use genawaiter::rc::Gen;
+use regex::Regex;
+use std::cmp::{self, Ordering};
+use std::collections::VecDeque;
+use std::collections::{BTreeMap, HashSet};
+use std::path::PathBuf;
+
+use crate::arithmetic_op;
+use crate::value::PointerEquality;
+use crate::vm::generators::{self, GenCo};
+use crate::warnings::WarningKind;
+use crate::{
+    errors::ErrorKind,
+    value::{CoercionKind, NixAttrs, NixList, NixString, SharedThunkSet, Thunk, Value},
+};
+
+use self::versions::{VersionPart, VersionPartsIter};
+
+mod to_xml;
+mod versions;
+
+#[cfg(feature = "impure")]
+mod impure;
+
+#[cfg(feature = "impure")]
+pub use impure::impure_builtins;
+
+// we set TVIX_CURRENT_SYSTEM in build.rs
+pub const CURRENT_PLATFORM: &str = env!("TVIX_CURRENT_SYSTEM");
+
+/// Coerce a Nix Value to a plain path, e.g. in order to access the
+/// file it points to via either `builtins.toPath` or an impure
+/// builtin. This coercion can _never_ be performed in a Nix program
+/// without using builtins (i.e. the trick `path: /. + path` to
+/// convert from a string to a path wouldn't hit this code).
+pub async fn coerce_value_to_path(co: &GenCo, v: Value) -> Result<PathBuf, ErrorKind> {
+    let value = generators::request_force(co, v).await;
+    if let Value::Path(p) = value {
+        return Ok(*p);
+    }
+
+    let vs = generators::request_string_coerce(co, value, CoercionKind::Weak).await;
+    let path = PathBuf::from(vs.as_str());
+    if path.is_absolute() {
+        Ok(path)
+    } else {
+        Err(ErrorKind::NotAnAbsolutePath(path))
+    }
+}
+
+#[builtins]
+mod pure_builtins {
+    use crate::value::PointerEquality;
+
+    use super::*;
+
+    #[builtin("abort")]
+    async fn builtin_abort(co: GenCo, message: Value) -> Result<Value, ErrorKind> {
+        Err(ErrorKind::Abort(message.to_str()?.to_string()))
+    }
+
+    #[builtin("add")]
+    async fn builtin_add(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        arithmetic_op!(&x, &y, +)
+    }
+
+    #[builtin("all")]
+    async fn builtin_all(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
+        for value in list.to_list()?.into_iter() {
+            let pred_result = generators::request_call_with(&co, pred.clone(), [value]).await;
+            let pred_result = generators::request_force(&co, pred_result).await;
+
+            if !pred_result.as_bool()? {
+                return Ok(Value::Bool(false));
+            }
+        }
+
+        Ok(Value::Bool(true))
+    }
+
+    #[builtin("any")]
+    async fn builtin_any(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
+        for value in list.to_list()?.into_iter() {
+            let pred_result = generators::request_call_with(&co, pred.clone(), [value]).await;
+            let pred_result = generators::request_force(&co, pred_result).await;
+
+            if pred_result.as_bool()? {
+                return Ok(Value::Bool(true));
+            }
+        }
+
+        Ok(Value::Bool(false))
+    }
+
+    #[builtin("attrNames")]
+    async fn builtin_attr_names(co: GenCo, set: Value) -> Result<Value, ErrorKind> {
+        let xs = set.to_attrs()?;
+        let mut output = Vec::with_capacity(xs.len());
+
+        for (key, _val) in xs.iter() {
+            output.push(Value::String(key.clone()));
+        }
+
+        Ok(Value::List(NixList::construct(output.len(), output)))
+    }
+
+    #[builtin("attrValues")]
+    async fn builtin_attr_values(co: GenCo, set: Value) -> Result<Value, ErrorKind> {
+        let xs = set.to_attrs()?;
+        let mut output = Vec::with_capacity(xs.len());
+
+        for (_key, val) in xs.iter() {
+            output.push(val.clone());
+        }
+
+        Ok(Value::List(NixList::construct(output.len(), output)))
+    }
+
+    #[builtin("baseNameOf")]
+    async fn builtin_base_name_of(co: GenCo, s: Value) -> Result<Value, ErrorKind> {
+        let s = s.coerce_to_string(co, CoercionKind::Weak).await?.to_str()?;
+        let result: String = s.rsplit_once('/').map(|(_, x)| x).unwrap_or(&s).into();
+        Ok(result.into())
+    }
+
+    #[builtin("bitAnd")]
+    async fn builtin_bit_and(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Integer(x.as_int()? & y.as_int()?))
+    }
+
+    #[builtin("bitOr")]
+    async fn builtin_bit_or(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Integer(x.as_int()? | y.as_int()?))
+    }
+
+    #[builtin("bitXor")]
+    async fn builtin_bit_xor(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Integer(x.as_int()? ^ y.as_int()?))
+    }
+
+    #[builtin("catAttrs")]
+    async fn builtin_cat_attrs(co: GenCo, key: Value, list: Value) -> Result<Value, ErrorKind> {
+        let key = key.to_str()?;
+        let list = list.to_list()?;
+        let mut output = vec![];
+
+        for item in list.into_iter() {
+            let set = generators::request_force(&co, item).await.to_attrs()?;
+
+            if let Some(value) = set.select(key.as_str()) {
+                output.push(value.clone());
+            }
+        }
+
+        Ok(Value::List(NixList::construct(output.len(), output)))
+    }
+
+    #[builtin("ceil")]
+    async fn builtin_ceil(co: GenCo, double: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Integer(double.as_float()?.ceil() as i64))
+    }
+
+    #[builtin("compareVersions")]
+    async fn builtin_compare_versions(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        let s1 = x.to_str()?;
+        let s1 = VersionPartsIter::new_for_cmp(s1.as_str());
+        let s2 = y.to_str()?;
+        let s2 = VersionPartsIter::new_for_cmp(s2.as_str());
+
+        match s1.cmp(s2) {
+            std::cmp::Ordering::Less => Ok(Value::Integer(-1)),
+            std::cmp::Ordering::Equal => Ok(Value::Integer(0)),
+            std::cmp::Ordering::Greater => Ok(Value::Integer(1)),
+        }
+    }
+
+    #[builtin("concatLists")]
+    async fn builtin_concat_lists(co: GenCo, lists: Value) -> Result<Value, ErrorKind> {
+        let mut out = imbl::Vector::new();
+
+        for value in lists.to_list()? {
+            let list = generators::request_force(&co, value).await.to_list()?;
+            out.extend(list.into_iter());
+        }
+
+        Ok(Value::List(out.into()))
+    }
+
+    #[builtin("concatMap")]
+    async fn builtin_concat_map(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> {
+        let list = list.to_list()?;
+        let mut res = imbl::Vector::new();
+        for val in list {
+            let out = generators::request_call_with(&co, f.clone(), [val]).await;
+            let out = generators::request_force(&co, out).await;
+            res.extend(out.to_list()?);
+        }
+        Ok(Value::List(res.into()))
+    }
+
+    #[builtin("concatStringsSep")]
+    async fn builtin_concat_strings_sep(
+        co: GenCo,
+        separator: Value,
+        list: Value,
+    ) -> Result<Value, ErrorKind> {
+        let separator = separator.to_str()?;
+        let list = list.to_list()?;
+        let mut res = String::new();
+        for (i, val) in list.into_iter().enumerate() {
+            if i != 0 {
+                res.push_str(&separator);
+            }
+            let s = generators::request_string_coerce(&co, val, CoercionKind::Weak).await;
+            res.push_str(s.as_str());
+        }
+        Ok(res.into())
+    }
+
+    #[builtin("deepSeq")]
+    async fn builtin_deep_seq(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        generators::request_deep_force(&co, x, SharedThunkSet::default()).await;
+        Ok(y)
+    }
+
+    #[builtin("div")]
+    async fn builtin_div(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        arithmetic_op!(&x, &y, /)
+    }
+
+    #[builtin("dirOf")]
+    async fn builtin_dir_of(co: GenCo, s: Value) -> Result<Value, ErrorKind> {
+        let is_path = s.is_path();
+        let str = s.coerce_to_string(co, CoercionKind::Weak).await?.to_str()?;
+        let result = str
+            .rsplit_once('/')
+            .map(|(x, _)| match x {
+                "" => "/",
+                _ => x,
+            })
+            .unwrap_or(".");
+        if is_path {
+            Ok(Value::Path(Box::new(result.into())))
+        } else {
+            Ok(result.into())
+        }
+    }
+
+    #[builtin("elem")]
+    async fn builtin_elem(co: GenCo, x: Value, xs: Value) -> Result<Value, ErrorKind> {
+        for val in xs.to_list()? {
+            if generators::check_equality(&co, x.clone(), val, PointerEquality::AllowAll).await? {
+                return Ok(true.into());
+            }
+        }
+        Ok(false.into())
+    }
+
+    #[builtin("elemAt")]
+    async fn builtin_elem_at(co: GenCo, xs: Value, i: Value) -> Result<Value, ErrorKind> {
+        let xs = xs.to_list()?;
+        let i = i.as_int()?;
+        if i < 0 {
+            Err(ErrorKind::IndexOutOfBounds { index: i })
+        } else {
+            match xs.get(i as usize) {
+                Some(x) => Ok(x.clone()),
+                None => Err(ErrorKind::IndexOutOfBounds { index: i }),
+            }
+        }
+    }
+
+    #[builtin("filter")]
+    async fn builtin_filter(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
+        let list: NixList = list.to_list()?;
+        let mut out = imbl::Vector::new();
+
+        for value in list {
+            let result = generators::request_call_with(&co, pred.clone(), [value.clone()]).await;
+
+            if generators::request_force(&co, result).await.as_bool()? {
+                out.push_back(value);
+            }
+        }
+
+        Ok(Value::List(out.into()))
+    }
+
+    #[builtin("floor")]
+    async fn builtin_floor(co: GenCo, double: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Integer(double.as_float()?.floor() as i64))
+    }
+
+    #[builtin("foldl'")]
+    async fn builtin_foldl(
+        co: GenCo,
+        op: Value,
+        #[lazy] nul: Value,
+        list: Value,
+    ) -> Result<Value, ErrorKind> {
+        let mut nul = nul;
+        let list = list.to_list()?;
+        for val in list {
+            // Every call of `op` is forced immediately, but `nul` is not, see
+            // https://github.com/NixOS/nix/blob/940e9eb8/src/libexpr/primops.cc#L3069-L3070C36
+            // and our tests for foldl'.
+            nul = generators::request_call_with(&co, op.clone(), [nul, val]).await;
+            nul = generators::request_force(&co, nul).await;
+        }
+
+        Ok(nul)
+    }
+
+    #[builtin("functionArgs")]
+    async fn builtin_function_args(co: GenCo, f: Value) -> Result<Value, ErrorKind> {
+        let lambda = &f.as_closure()?.lambda();
+        let formals = if let Some(formals) = &lambda.formals {
+            formals
+        } else {
+            return Ok(Value::attrs(NixAttrs::empty()));
+        };
+        Ok(Value::attrs(NixAttrs::from_iter(
+            formals.arguments.iter().map(|(k, v)| (k.clone(), (*v))),
+        )))
+    }
+
+    #[builtin("fromJSON")]
+    async fn builtin_from_json(co: GenCo, json: Value) -> Result<Value, ErrorKind> {
+        let json_str = json.to_str()?;
+
+        serde_json::from_str(&json_str).map_err(|err| err.into())
+    }
+
+    #[builtin("toJSON")]
+    async fn builtin_to_json(co: GenCo, val: Value) -> Result<Value, ErrorKind> {
+        let json_value = val.to_json(&co).await?;
+        let json_str = serde_json::to_string(&json_value)?;
+        Ok(json_str.into())
+    }
+
+    #[builtin("fromTOML")]
+    async fn builtin_from_toml(co: GenCo, toml: Value) -> Result<Value, ErrorKind> {
+        let toml_str = toml.to_str()?;
+
+        toml::from_str(&toml_str).map_err(|err| err.into())
+    }
+
+    #[builtin("genericClosure")]
+    async fn builtin_generic_closure(co: GenCo, input: Value) -> Result<Value, ErrorKind> {
+        let attrs = input.to_attrs()?;
+
+        // The work set is maintained as a VecDeque because new items
+        // are popped from the front.
+        let mut work_set: VecDeque<Value> =
+            generators::request_force(&co, attrs.select_required("startSet")?.clone())
+                .await
+                .to_list()?
+                .into_iter()
+                .collect();
+
+        let operator = attrs.select_required("operator")?;
+
+        let mut res = imbl::Vector::new();
+        let mut done_keys: Vec<Value> = vec![];
+
+        while let Some(val) = work_set.pop_front() {
+            let val = generators::request_force(&co, val).await;
+            let attrs = val.to_attrs()?;
+            let key = attrs.select_required("key")?;
+
+            if !bgc_insert_key(&co, key.clone(), &mut done_keys).await? {
+                continue;
+            }
+
+            res.push_back(val.clone());
+
+            let op_result = generators::request_force(
+                &co,
+                generators::request_call_with(&co, operator.clone(), [val]).await,
+            )
+            .await;
+
+            work_set.extend(op_result.to_list()?.into_iter());
+        }
+
+        Ok(Value::List(NixList::from(res)))
+    }
+
+    #[builtin("genList")]
+    async fn builtin_gen_list(
+        co: GenCo,
+        generator: Value,
+        length: Value,
+    ) -> Result<Value, ErrorKind> {
+        let mut out = imbl::Vector::<Value>::new();
+        let len = length.as_int()?;
+        // the best span we can get…
+        let span = generators::request_span(&co).await;
+
+        for i in 0..len {
+            let val = Value::Thunk(Thunk::new_suspended_call(
+                generator.clone(),
+                i.into(),
+                span.clone(),
+            ));
+            out.push_back(val);
+        }
+
+        Ok(Value::List(out.into()))
+    }
+
+    #[builtin("getAttr")]
+    async fn builtin_get_attr(co: GenCo, key: Value, set: Value) -> Result<Value, ErrorKind> {
+        let k = key.to_str()?;
+        let xs = set.to_attrs()?;
+
+        match xs.select(k.as_str()) {
+            Some(x) => Ok(x.clone()),
+            None => Err(ErrorKind::AttributeNotFound {
+                name: k.to_string(),
+            }),
+        }
+    }
+
+    #[builtin("groupBy")]
+    async fn builtin_group_by(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> {
+        let mut res: BTreeMap<NixString, imbl::Vector<Value>> = BTreeMap::new();
+        for val in list.to_list()? {
+            let key = generators::request_force(
+                &co,
+                generators::request_call_with(&co, f.clone(), [val.clone()]).await,
+            )
+            .await
+            .to_str()?;
+
+            res.entry(key)
+                .or_insert_with(imbl::Vector::new)
+                .push_back(val);
+        }
+        Ok(Value::attrs(NixAttrs::from_iter(
+            res.into_iter()
+                .map(|(k, v)| (k, Value::List(NixList::from(v)))),
+        )))
+    }
+
+    #[builtin("hasAttr")]
+    async fn builtin_has_attr(co: GenCo, key: Value, set: Value) -> Result<Value, ErrorKind> {
+        let k = key.to_str()?;
+        let xs = set.to_attrs()?;
+
+        Ok(Value::Bool(xs.contains(k.as_str())))
+    }
+
+    #[builtin("head")]
+    async fn builtin_head(co: GenCo, list: Value) -> Result<Value, ErrorKind> {
+        match list.to_list()?.get(0) {
+            Some(x) => Ok(x.clone()),
+            None => Err(ErrorKind::IndexOutOfBounds { index: 0 }),
+        }
+    }
+
+    #[builtin("intersectAttrs")]
+    async fn builtin_intersect_attrs(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        let attrs1 = x.to_attrs()?;
+        let attrs2 = y.to_attrs()?;
+        let res = attrs2.iter().filter_map(|(k, v)| {
+            if attrs1.contains(k) {
+                Some((k.clone(), v.clone()))
+            } else {
+                None
+            }
+        });
+        Ok(Value::attrs(NixAttrs::from_iter(res)))
+    }
+
+    #[builtin("isAttrs")]
+    async fn builtin_is_attrs(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::Attrs(_))))
+    }
+
+    #[builtin("isBool")]
+    async fn builtin_is_bool(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::Bool(_))))
+    }
+
+    #[builtin("isFloat")]
+    async fn builtin_is_float(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::Float(_))))
+    }
+
+    #[builtin("isFunction")]
+    async fn builtin_is_function(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(
+            value,
+            Value::Closure(_) | Value::Builtin(_)
+        )))
+    }
+
+    #[builtin("isInt")]
+    async fn builtin_is_int(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::Integer(_))))
+    }
+
+    #[builtin("isList")]
+    async fn builtin_is_list(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::List(_))))
+    }
+
+    #[builtin("isNull")]
+    async fn builtin_is_null(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::Null)))
+    }
+
+    #[builtin("isPath")]
+    async fn builtin_is_path(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::Path(_))))
+    }
+
+    #[builtin("isString")]
+    async fn builtin_is_string(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(value, Value::String(_))))
+    }
+
+    #[builtin("length")]
+    async fn builtin_length(co: GenCo, list: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Integer(list.to_list()?.len() as i64))
+    }
+
+    #[builtin("lessThan")]
+    async fn builtin_less_than(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::Bool(matches!(
+            x.nix_cmp_ordering(y, co).await?,
+            Some(Ordering::Less)
+        )))
+    }
+
+    #[builtin("listToAttrs")]
+    async fn builtin_list_to_attrs(co: GenCo, list: Value) -> Result<Value, ErrorKind> {
+        let list = list.to_list()?;
+        let mut map = BTreeMap::new();
+        for val in list {
+            let attrs = generators::request_force(&co, val).await.to_attrs()?;
+            let name = generators::request_force(&co, attrs.select_required("name")?.clone())
+                .await
+                .to_str()?;
+            let value = attrs.select_required("value")?.clone();
+            // Map entries earlier in the list take precedence over entries later in the list
+            map.entry(name).or_insert(value);
+        }
+        Ok(Value::attrs(NixAttrs::from_iter(map.into_iter())))
+    }
+
+    #[builtin("map")]
+    async fn builtin_map(co: GenCo, f: Value, list: Value) -> Result<Value, ErrorKind> {
+        let mut out = imbl::Vector::<Value>::new();
+
+        // the best span we can get…
+        let span = generators::request_span(&co).await;
+
+        for val in list.to_list()? {
+            let result = Value::Thunk(Thunk::new_suspended_call(f.clone(), val, span.clone()));
+            out.push_back(result)
+        }
+
+        Ok(Value::List(out.into()))
+    }
+
+    #[builtin("mapAttrs")]
+    async fn builtin_map_attrs(co: GenCo, f: Value, attrs: Value) -> Result<Value, ErrorKind> {
+        let attrs = attrs.to_attrs()?;
+        let mut out = imbl::OrdMap::new();
+
+        // the best span we can get…
+        let span = generators::request_span(&co).await;
+
+        for (key, value) in attrs.into_iter() {
+            let result = Value::Thunk(Thunk::new_suspended_call(
+                f.clone(),
+                key.clone().into(),
+                span.clone(),
+            ));
+            let result = Value::Thunk(Thunk::new_suspended_call(result, value, span.clone()));
+
+            out.insert(key, result);
+        }
+
+        Ok(Value::attrs(out.into()))
+    }
+
+    #[builtin("match")]
+    async fn builtin_match(co: GenCo, regex: Value, str: Value) -> Result<Value, ErrorKind> {
+        let s = str.to_str()?;
+        let re = regex.to_str()?;
+        let re: Regex = Regex::new(&format!("^{}$", re.as_str())).unwrap();
+        match re.captures(&s) {
+            Some(caps) => Ok(Value::List(
+                caps.iter()
+                    .skip(1)
+                    .map(|grp| grp.map(|g| Value::from(g.as_str())).unwrap_or(Value::Null))
+                    .collect::<imbl::Vector<Value>>()
+                    .into(),
+            )),
+            None => Ok(Value::Null),
+        }
+    }
+
+    #[builtin("mul")]
+    async fn builtin_mul(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        arithmetic_op!(&x, &y, *)
+    }
+
+    #[builtin("parseDrvName")]
+    async fn builtin_parse_drv_name(co: GenCo, s: Value) -> Result<Value, ErrorKind> {
+        // This replicates cppnix's (mis?)handling of codepoints
+        // above U+007f following 0x2d ('-')
+        let s = s.to_str()?;
+        let slice: &[u8] = s.as_str().as_ref();
+        let (name, dash_and_version) = slice.split_at(
+            slice
+                .windows(2)
+                .enumerate()
+                .find_map(|x| match x {
+                    (idx, [b'-', c1]) if !c1.is_ascii_alphabetic() => Some(idx),
+                    _ => None,
+                })
+                .unwrap_or(slice.len()),
+        );
+        let version = dash_and_version
+            .split_first()
+            .map(|x| core::str::from_utf8(x.1))
+            .unwrap_or(Ok(""))?;
+        Ok(Value::attrs(NixAttrs::from_iter(
+            [("name", core::str::from_utf8(name)?), ("version", version)].into_iter(),
+        )))
+    }
+
+    #[builtin("partition")]
+    async fn builtin_partition(co: GenCo, pred: Value, list: Value) -> Result<Value, ErrorKind> {
+        let mut right: imbl::Vector<Value> = Default::default();
+        let mut wrong: imbl::Vector<Value> = Default::default();
+
+        let list: NixList = list.to_list()?;
+        for elem in list {
+            let result = generators::request_call_with(&co, pred.clone(), [elem.clone()]).await;
+
+            if generators::request_force(&co, result).await.as_bool()? {
+                right.push_back(elem);
+            } else {
+                wrong.push_back(elem);
+            };
+        }
+
+        let res = [
+            ("right", Value::List(NixList::from(right))),
+            ("wrong", Value::List(NixList::from(wrong))),
+        ];
+
+        Ok(Value::attrs(NixAttrs::from_iter(res.into_iter())))
+    }
+
+    #[builtin("removeAttrs")]
+    async fn builtin_remove_attrs(
+        co: GenCo,
+        attrs: Value,
+        keys: Value,
+    ) -> Result<Value, ErrorKind> {
+        let attrs = attrs.to_attrs()?;
+        let keys = keys
+            .to_list()?
+            .into_iter()
+            .map(|v| v.to_str())
+            .collect::<Result<HashSet<_>, _>>()?;
+        let res = attrs.iter().filter_map(|(k, v)| {
+            if !keys.contains(k) {
+                Some((k.clone(), v.clone()))
+            } else {
+                None
+            }
+        });
+        Ok(Value::attrs(NixAttrs::from_iter(res)))
+    }
+
+    #[builtin("replaceStrings")]
+    async fn builtin_replace_strings(
+        co: GenCo,
+        from: Value,
+        to: Value,
+        s: Value,
+    ) -> Result<Value, ErrorKind> {
+        let from = from.to_list()?;
+        for val in &from {
+            generators::request_force(&co, val.clone()).await;
+        }
+
+        let to = to.to_list()?;
+        for val in &to {
+            generators::request_force(&co, val.clone()).await;
+        }
+
+        let string = s.to_str()?;
+
+        let mut res = String::new();
+
+        let mut i: usize = 0;
+        let mut empty_string_replace = false;
+
+        // This can't be implemented using Rust's string.replace() as
+        // well as a map because we need to handle errors with results
+        // as well as "reset" the iterator to zero for the replacement
+        // everytime there's a successful match.
+        // Also, Rust's string.replace allocates a new string
+        // on every call which is not preferable.
+        'outer: while i < string.len() {
+            // Try a match in all the from strings
+            for elem in std::iter::zip(from.iter(), to.iter()) {
+                let from = elem.0.to_str()?;
+                let to = elem.1.to_str()?;
+
+                if i + from.len() >= string.len() {
+                    continue;
+                }
+
+                // We already applied a from->to with an empty from
+                // transformation.
+                // Let's skip it so that we don't loop infinitely
+                if empty_string_replace && from.as_str().is_empty() {
+                    continue;
+                }
+
+                // if we match the `from` string, let's replace
+                if &string[i..i + from.len()] == from.as_str() {
+                    res += &to;
+                    i += from.len();
+
+                    // remember if we applied the empty from->to
+                    empty_string_replace = from.as_str().is_empty();
+
+                    continue 'outer;
+                }
+            }
+
+            // If we don't match any `from`, we simply add a character
+            res += &string[i..i + 1];
+            i += 1;
+
+            // Since we didn't apply anything transformation,
+            // we reset the empty string replacement
+            empty_string_replace = false;
+        }
+
+        // Special case when the string is empty or at the string's end
+        // and one of the from is also empty
+        for elem in std::iter::zip(from.iter(), to.iter()) {
+            let from = elem.0.to_str()?;
+            let to = elem.1.to_str()?;
+
+            if from.as_str().is_empty() {
+                res += &to;
+                break;
+            }
+        }
+        Ok(Value::String(res.into()))
+    }
+
+    #[builtin("seq")]
+    async fn builtin_seq(co: GenCo, _x: Value, y: Value) -> Result<Value, ErrorKind> {
+        // The builtin calling infra has already forced both args for us, so
+        // we just return the second and ignore the first
+        Ok(y)
+    }
+
+    #[builtin("split")]
+    async fn builtin_split(co: GenCo, regex: Value, str: Value) -> Result<Value, ErrorKind> {
+        let s = str.to_str()?;
+        let text = s.as_str();
+        let re = regex.to_str()?;
+        let re: Regex = Regex::new(re.as_str()).unwrap();
+        let mut capture_locations = re.capture_locations();
+        let num_captures = capture_locations.len();
+        let mut ret = imbl::Vector::new();
+        let mut pos = 0;
+
+        while let Some(thematch) = re.captures_read_at(&mut capture_locations, text, pos) {
+            // push the unmatched characters preceding the match
+            ret.push_back(Value::from(&text[pos..thematch.start()]));
+
+            // Push a list with one element for each capture
+            // group in the regex, containing the characters
+            // matched by that capture group, or null if no match.
+            // We skip capture 0; it represents the whole match.
+            let v: imbl::Vector<Value> = (1..num_captures)
+                .map(|i| capture_locations.get(i))
+                .map(|o| {
+                    o.map(|(start, end)| Value::from(&text[start..end]))
+                        .unwrap_or(Value::Null)
+                })
+                .collect();
+            ret.push_back(Value::List(NixList::from(v)));
+            pos = thematch.end();
+        }
+
+        // push the unmatched characters following the last match
+        ret.push_back(Value::from(&text[pos..]));
+
+        Ok(Value::List(NixList::from(ret)))
+    }
+
+    #[builtin("sort")]
+    async fn builtin_sort(co: GenCo, comparator: Value, list: Value) -> Result<Value, ErrorKind> {
+        let list = list.to_list()?;
+        let sorted = list.sort_by(&co, comparator).await?;
+        Ok(Value::List(sorted))
+    }
+
+    #[builtin("splitVersion")]
+    async fn builtin_split_version(co: GenCo, s: Value) -> Result<Value, ErrorKind> {
+        let s = s.to_str()?;
+        let s = VersionPartsIter::new(s.as_str());
+
+        let parts = s
+            .map(|s| {
+                Value::String(match s {
+                    VersionPart::Number(n) => n.into(),
+                    VersionPart::Word(w) => w.into(),
+                })
+            })
+            .collect::<Vec<Value>>();
+        Ok(Value::List(NixList::construct(parts.len(), parts)))
+    }
+
+    #[builtin("stringLength")]
+    async fn builtin_string_length(co: GenCo, #[lazy] s: Value) -> Result<Value, ErrorKind> {
+        // also forces the value
+        let s = s.coerce_to_string(co, CoercionKind::Weak).await?;
+        Ok(Value::Integer(s.to_str()?.as_str().len() as i64))
+    }
+
+    #[builtin("sub")]
+    async fn builtin_sub(co: GenCo, x: Value, y: Value) -> Result<Value, ErrorKind> {
+        arithmetic_op!(&x, &y, -)
+    }
+
+    #[builtin("substring")]
+    async fn builtin_substring(
+        co: GenCo,
+        start: Value,
+        len: Value,
+        s: Value,
+    ) -> Result<Value, ErrorKind> {
+        let beg = start.as_int()?;
+        let len = len.as_int()?;
+        let x = s.coerce_to_string(co, CoercionKind::Weak).await?.to_str()?;
+
+        if beg < 0 {
+            return Err(ErrorKind::IndexOutOfBounds { index: beg });
+        }
+        let beg = beg as usize;
+
+        // Nix doesn't assert that the length argument is
+        // non-negative when the starting index is GTE the
+        // string's length.
+        if beg >= x.as_str().len() {
+            return Ok(Value::String("".into()));
+        }
+
+        if len < 0 {
+            return Err(ErrorKind::NegativeLength { length: len });
+        }
+
+        let len = len as usize;
+        let end = cmp::min(beg + len, x.as_str().len());
+
+        Ok(Value::String(x.as_str()[beg..end].into()))
+    }
+
+    #[builtin("tail")]
+    async fn builtin_tail(co: GenCo, list: Value) -> Result<Value, ErrorKind> {
+        let xs = list.to_list()?;
+
+        if xs.is_empty() {
+            Err(ErrorKind::TailEmptyList)
+        } else {
+            let output = xs.into_iter().skip(1).collect::<Vec<_>>();
+            Ok(Value::List(NixList::construct(output.len(), output)))
+        }
+    }
+
+    #[builtin("throw")]
+    async fn builtin_throw(co: GenCo, message: Value) -> Result<Value, ErrorKind> {
+        Err(ErrorKind::Throw(message.to_str()?.to_string()))
+    }
+
+    #[builtin("toString")]
+    async fn builtin_to_string(co: GenCo, #[lazy] x: Value) -> Result<Value, ErrorKind> {
+        // coerce_to_string forces for us
+        x.coerce_to_string(co, CoercionKind::Strong).await
+    }
+
+    #[builtin("toXML")]
+    async fn builtin_to_xml(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+        let value = generators::request_deep_force(&co, value, SharedThunkSet::default()).await;
+        let mut buf: Vec<u8> = vec![];
+        to_xml::value_to_xml(&mut buf, &value)?;
+        Ok(String::from_utf8(buf)?.into())
+    }
+
+    #[builtin("placeholder")]
+    async fn builtin_placeholder(co: GenCo, #[lazy] _x: Value) -> Result<Value, ErrorKind> {
+        generators::emit_warning_kind(&co, WarningKind::NotImplemented("builtins.placeholder"))
+            .await;
+        Ok("<builtins.placeholder-is-not-implemented-in-tvix-yet>".into())
+    }
+
+    #[builtin("trace")]
+    async fn builtin_trace(co: GenCo, message: Value, value: Value) -> Result<Value, ErrorKind> {
+        // TODO(grfn): `trace` should be pluggable and capturable, probably via a method on
+        // the VM
+        eprintln!("trace: {} :: {}", message, message.type_of());
+        Ok(value)
+    }
+
+    #[builtin("toPath")]
+    async fn builtin_to_path(co: GenCo, s: Value) -> Result<Value, ErrorKind> {
+        let path: Value = crate::value::canon_path(coerce_value_to_path(&co, s).await?).into();
+        Ok(path.coerce_to_string(co, CoercionKind::Weak).await?)
+    }
+
+    #[builtin("tryEval")]
+    async fn builtin_try_eval(co: GenCo, #[lazy] e: Value) -> Result<Value, ErrorKind> {
+        let res = match generators::request_try_force(&co, e).await {
+            Some(value) => [("value", value), ("success", true.into())],
+            None => [("value", false.into()), ("success", false.into())],
+        };
+
+        Ok(Value::attrs(NixAttrs::from_iter(res.into_iter())))
+    }
+
+    #[builtin("typeOf")]
+    async fn builtin_type_of(co: GenCo, x: Value) -> Result<Value, ErrorKind> {
+        Ok(Value::String(x.type_of().into()))
+    }
+}
+
+/// Internal helper function for genericClosure, determining whether a
+/// value has been seen before.
+async fn bgc_insert_key(co: &GenCo, key: Value, done: &mut Vec<Value>) -> Result<bool, ErrorKind> {
+    for existing in done.iter() {
+        if generators::check_equality(
+            co,
+            existing.clone(),
+            key.clone(),
+            // TODO(tazjin): not actually sure which semantics apply here
+            PointerEquality::ForbidAll,
+        )
+        .await?
+        {
+            return Ok(false);
+        }
+    }
+
+    done.push(key);
+    Ok(true)
+}
+
+/// The set of standard pure builtins in Nix, mostly concerned with
+/// data structure manipulation (string, attrs, list, etc. functions).
+pub fn pure_builtins() -> Vec<(&'static str, Value)> {
+    let mut result = pure_builtins::builtins();
+
+    // Pure-value builtins
+    result.push(("nixVersion", Value::String("2.3-compat-tvix-0.1".into())));
+    result.push(("langVersion", Value::Integer(6)));
+    result.push(("null", Value::Null));
+    result.push(("true", Value::Bool(true)));
+    result.push(("false", Value::Bool(false)));
+
+    result.push((
+        "currentSystem",
+        crate::systems::llvm_triple_to_nix_double(CURRENT_PLATFORM).into(),
+    ));
+
+    result
+}
+
+#[builtins]
+mod placeholder_builtins {
+    use super::*;
+
+    #[builtin("unsafeDiscardStringContext")]
+    async fn builtin_unsafe_discard_string_context(
+        _: GenCo,
+        #[lazy] s: Value,
+    ) -> Result<Value, ErrorKind> {
+        // Tvix does not manually track contexts, and this is a no-op for us.
+        Ok(s)
+    }
+
+    #[builtin("addErrorContext")]
+    async fn builtin_add_error_context(
+        co: GenCo,
+        #[lazy] _context: Value,
+        #[lazy] val: Value,
+    ) -> Result<Value, ErrorKind> {
+        generators::emit_warning_kind(&co, WarningKind::NotImplemented("builtins.addErrorContext"))
+            .await;
+        Ok(val)
+    }
+
+    #[builtin("unsafeGetAttrPos")]
+    async fn builtin_unsafe_get_attr_pos(
+        co: GenCo,
+        _name: Value,
+        _attrset: Value,
+    ) -> Result<Value, ErrorKind> {
+        generators::emit_warning_kind(
+            &co,
+            WarningKind::NotImplemented("builtins.unsafeGetAttrsPos"),
+        )
+        .await;
+        let res = [
+            ("line", 42.into()),
+            ("col", 42.into()),
+            ("file", Value::Path(Box::new("/deep/thought".into()))),
+        ];
+        Ok(Value::attrs(NixAttrs::from_iter(res.into_iter())))
+    }
+}
+
+pub fn placeholders() -> Vec<(&'static str, Value)> {
+    placeholder_builtins::builtins()
+}
diff --git a/tvix/eval/src/builtins/to_xml.rs b/tvix/eval/src/builtins/to_xml.rs
new file mode 100644
index 000000000000..375785b7a63e
--- /dev/null
+++ b/tvix/eval/src/builtins/to_xml.rs
@@ -0,0 +1,139 @@
+//! This module implements `builtins.toXML`, which is a serialisation
+//! of value information as well as internal tvix state that several
+//! things in nixpkgs rely on.
+
+use std::{io::Write, rc::Rc};
+use xml::writer::events::XmlEvent;
+use xml::writer::EmitterConfig;
+use xml::writer::EventWriter;
+
+use crate::{ErrorKind, Value};
+
+/// Recursively serialise a value to XML. The value *must* have been
+/// deep-forced before being passed to this function.
+pub(super) fn value_to_xml<W: Write>(mut writer: W, value: &Value) -> Result<(), ErrorKind> {
+    let config = EmitterConfig {
+        perform_indent: true,
+        pad_self_closing: true,
+
+        // Nix uses single-quotes *only* in the document declaration,
+        // so we need to write it manually.
+        write_document_declaration: false,
+        ..Default::default()
+    };
+
+    // Write a literal document declaration, using C++-Nix-style
+    // single quotes.
+    writeln!(writer, "<?xml version='1.0' encoding='utf-8'?>")?;
+
+    let mut writer = EventWriter::new_with_config(writer, config);
+
+    writer.write(XmlEvent::start_element("expr"))?;
+    value_variant_to_xml(&mut writer, value)?;
+    writer.write(XmlEvent::end_element())?;
+
+    // Unwrap the writer to add the final newline that C++ Nix adds.
+    writeln!(writer.into_inner())?;
+
+    Ok(())
+}
+
+fn write_typed_value<W: Write, V: ToString>(
+    w: &mut EventWriter<W>,
+    name: &str,
+    value: V,
+) -> Result<(), ErrorKind> {
+    w.write(XmlEvent::start_element(name).attr("value", &value.to_string()))?;
+    w.write(XmlEvent::end_element())?;
+    Ok(())
+}
+
+fn value_variant_to_xml<W: Write>(w: &mut EventWriter<W>, value: &Value) -> Result<(), ErrorKind> {
+    match value {
+        Value::Thunk(t) => return value_variant_to_xml(w, &t.value()),
+
+        Value::Null => {
+            w.write(XmlEvent::start_element("null"))?;
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Bool(b) => return write_typed_value(w, "bool", b),
+        Value::Integer(i) => return write_typed_value(w, "int", i),
+        Value::Float(f) => return write_typed_value(w, "float", f),
+        Value::String(s) => return write_typed_value(w, "string", s.as_str()),
+        Value::Path(p) => return write_typed_value(w, "path", p.to_string_lossy()),
+
+        Value::List(list) => {
+            w.write(XmlEvent::start_element("list"))?;
+
+            for elem in list.into_iter() {
+                value_variant_to_xml(w, elem)?;
+            }
+
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Attrs(attrs) => {
+            w.write(XmlEvent::start_element("attrs"))?;
+
+            for elem in attrs.iter() {
+                w.write(XmlEvent::start_element("attr").attr("name", elem.0.as_str()))?;
+                value_variant_to_xml(w, elem.1)?;
+                w.write(XmlEvent::end_element())?;
+            }
+
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Closure(c) => {
+            w.write(XmlEvent::start_element("function"))?;
+
+            match &c.lambda.formals {
+                Some(formals) => {
+                    if formals.ellipsis {
+                        w.write(XmlEvent::start_element("attrspat").attr("ellipsis", "1"))?;
+                        w.write(XmlEvent::end_element())?;
+                    }
+
+                    for arg in formals.arguments.iter() {
+                        w.write(XmlEvent::start_element("attr").attr("name", arg.0.as_str()))?;
+                        w.write(XmlEvent::end_element())?;
+                    }
+                }
+                None => {
+                    // TODO(tazjin): tvix does not currently persist function
+                    // argument names anywhere (whereas we do for formals, as
+                    // that is required for other runtime behaviour). Because of
+                    // this the implementation here is fake, always returning
+                    // the same argument name.
+                    //
+                    // If we don't want to persist the data, we can re-parse the
+                    // AST from the spans of the lambda's bytecode and figure it
+                    // out that way, but it needs some investigating.
+                    w.write(XmlEvent::start_element("varpat").attr("name", /* fake: */ "x"))?;
+                    w.write(XmlEvent::end_element())?;
+                }
+            }
+
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Builtin(_) => {
+            w.write(XmlEvent::start_element("unevaluated"))?;
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::AttrNotFound
+        | Value::Blueprint(_)
+        | Value::DeferredUpvalue(_)
+        | Value::UnresolvedPath(_)
+        | Value::Json(_) => {
+            return Err(ErrorKind::TvixBug {
+                msg: "internal value variant encountered in builtins.toXML",
+                metadata: Some(Rc::new(value.clone())),
+            })
+        }
+    }?;
+
+    Ok(())
+}
diff --git a/tvix/eval/src/builtins/versions.rs b/tvix/eval/src/builtins/versions.rs
new file mode 100644
index 000000000000..79fb82b868fb
--- /dev/null
+++ b/tvix/eval/src/builtins/versions.rs
@@ -0,0 +1,155 @@
+use std::cmp::Ordering;
+use std::iter::{once, Chain, Once};
+use std::ops::RangeInclusive;
+
+/// Version strings can be broken up into Parts.
+/// One Part represents either a string of digits or characters.
+/// '.' and '_' represent deviders between parts and are not included in any part.
+#[derive(PartialEq, Eq, Clone, Debug)]
+pub enum VersionPart<'a> {
+    Word(&'a str),
+    Number(&'a str),
+}
+
+impl PartialOrd for VersionPart<'_> {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl Ord for VersionPart<'_> {
+    fn cmp(&self, other: &Self) -> Ordering {
+        match (self, other) {
+            (VersionPart::Number(s1), VersionPart::Number(s2)) => {
+                // Note: C++ Nix uses `int`, but probably doesn't make a difference
+                // We trust that the splitting was done correctly and parsing will work
+                let n1: u64 = s1.parse().unwrap();
+                let n2: u64 = s2.parse().unwrap();
+                n1.cmp(&n2)
+            }
+
+            // `pre` looses unless the other part is also a `pre`
+            (VersionPart::Word("pre"), VersionPart::Word("pre")) => Ordering::Equal,
+            (VersionPart::Word("pre"), _) => Ordering::Less,
+            (_, VersionPart::Word("pre")) => Ordering::Greater,
+
+            // Number wins against Word
+            (VersionPart::Number(_), VersionPart::Word(_)) => Ordering::Greater,
+            (VersionPart::Word(_), VersionPart::Number(_)) => Ordering::Less,
+
+            (VersionPart::Word(w1), VersionPart::Word(w2)) => w1.cmp(w2),
+        }
+    }
+}
+
+/// Type used to hold information about a VersionPart during creation
+enum InternalPart {
+    Number { range: RangeInclusive<usize> },
+    Word { range: RangeInclusive<usize> },
+    Break,
+}
+
+/// An iterator which yields the parts of a version string.
+///
+/// This can then be directly used to compare two versions
+pub struct VersionPartsIter<'a> {
+    cached_part: InternalPart,
+    iter: std::str::CharIndices<'a>,
+    version: &'a str,
+}
+
+impl<'a> VersionPartsIter<'a> {
+    pub fn new(version: &'a str) -> Self {
+        Self {
+            cached_part: InternalPart::Break,
+            iter: version.char_indices(),
+            version,
+        }
+    }
+
+    /// Create an iterator that yields all version parts followed by an additional
+    /// `VersionPart::Word("")` part (i.e. you can think of this as
+    /// `builtins.splitVersion version ++ [ "" ]`). This is necessary, because
+    /// Nix's `compareVersions` is not entirely lexicographical: If we have two
+    /// equal versions, but one is longer, the longer one is only considered
+    /// greater if the first additional part of the longer version is not `pre`,
+    /// e.g. `2.3 > 2.3pre`. It is otherwise lexicographical, so peculiar behavior
+    /// like `2.3 < 2.3.0pre` ensues. Luckily for us, this means that we can
+    /// lexicographically compare two version strings, _if_ we append an extra
+    /// component to both versions.
+    pub fn new_for_cmp(version: &'a str) -> Chain<Self, Once<VersionPart>> {
+        Self::new(version).chain(once(VersionPart::Word("")))
+    }
+}
+
+impl<'a> Iterator for VersionPartsIter<'a> {
+    type Item = VersionPart<'a>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let char = self.iter.next();
+
+        if char.is_none() {
+            let cached_part = std::mem::replace(&mut self.cached_part, InternalPart::Break);
+            match cached_part {
+                InternalPart::Break => return None,
+                InternalPart::Number { range } => {
+                    return Some(VersionPart::Number(&self.version[range]))
+                }
+                InternalPart::Word { range } => {
+                    return Some(VersionPart::Word(&self.version[range]))
+                }
+            }
+        }
+
+        let (pos, char) = char.unwrap();
+        match char {
+            // Divider encountered
+            '.' | '-' => {
+                let cached_part = std::mem::replace(&mut self.cached_part, InternalPart::Break);
+                match cached_part {
+                    InternalPart::Number { range } => {
+                        Some(VersionPart::Number(&self.version[range]))
+                    }
+                    InternalPart::Word { range } => Some(VersionPart::Word(&self.version[range])),
+                    InternalPart::Break => self.next(),
+                }
+            }
+
+            // digit encountered
+            _ if char.is_ascii_digit() => {
+                let cached_part = std::mem::replace(
+                    &mut self.cached_part,
+                    InternalPart::Number { range: pos..=pos },
+                );
+                match cached_part {
+                    InternalPart::Number { range } => {
+                        self.cached_part = InternalPart::Number {
+                            range: *range.start()..=*range.end() + 1,
+                        };
+                        self.next()
+                    }
+                    InternalPart::Word { range } => Some(VersionPart::Word(&self.version[range])),
+                    InternalPart::Break => self.next(),
+                }
+            }
+
+            // char encountered
+            _ => {
+                let mut cached_part = InternalPart::Word { range: pos..=pos };
+                std::mem::swap(&mut cached_part, &mut self.cached_part);
+                match cached_part {
+                    InternalPart::Word { range } => {
+                        self.cached_part = InternalPart::Word {
+                            range: *range.start()..=*range.end() + char.len_utf8(),
+                        };
+                        self.next()
+                    }
+                    InternalPart::Number { range } => {
+                        Some(VersionPart::Number(&self.version[range]))
+                    }
+                    InternalPart::Break => self.next(),
+                }
+            }
+        }
+    }
+}
diff --git a/tvix/eval/src/chunk.rs b/tvix/eval/src/chunk.rs
new file mode 100644
index 000000000000..9d35b30c64a6
--- /dev/null
+++ b/tvix/eval/src/chunk.rs
@@ -0,0 +1,279 @@
+use std::io::Write;
+use std::ops::{Index, IndexMut};
+
+use crate::opcode::{CodeIdx, ConstantIdx, OpCode};
+use crate::value::Value;
+use crate::SourceCode;
+
+/// Represents a source location from which one or more operations
+/// were compiled.
+///
+/// The span itself is an index into a [codemap::Codemap], and the
+/// structure tracks the number of operations that were yielded from
+/// the same span.
+///
+/// At error reporting time, it becomes possible to either just fetch
+/// the textual representation of that span from the codemap, or to
+/// even re-parse the AST using rnix to create more semantically
+/// interesting errors.
+#[derive(Clone, Debug, PartialEq)]
+struct SourceSpan {
+    /// Span into the [codemap::Codemap].
+    span: codemap::Span,
+
+    /// Index of the first operation covered by this span.
+    start: usize,
+}
+
+/// A chunk is a representation of a sequence of bytecode
+/// instructions, associated constants and additional metadata as
+/// emitted by the compiler.
+#[derive(Debug, Default)]
+pub struct Chunk {
+    pub code: Vec<OpCode>,
+    pub constants: Vec<Value>,
+    spans: Vec<SourceSpan>,
+}
+
+impl Index<ConstantIdx> for Chunk {
+    type Output = Value;
+
+    fn index(&self, index: ConstantIdx) -> &Self::Output {
+        &self.constants[index.0]
+    }
+}
+
+impl Index<CodeIdx> for Chunk {
+    type Output = OpCode;
+
+    fn index(&self, index: CodeIdx) -> &Self::Output {
+        &self.code[index.0]
+    }
+}
+
+impl IndexMut<CodeIdx> for Chunk {
+    fn index_mut(&mut self, index: CodeIdx) -> &mut Self::Output {
+        &mut self.code[index.0]
+    }
+}
+
+impl Chunk {
+    pub fn push_op(&mut self, data: OpCode, span: codemap::Span) -> CodeIdx {
+        let idx = self.code.len();
+        self.code.push(data);
+        self.push_span(span, idx);
+        CodeIdx(idx)
+    }
+
+    /// Get the first span of a chunk, no questions asked.
+    pub fn first_span(&self) -> codemap::Span {
+        self.spans[0].span
+    }
+
+    /// Pop the last operation from the chunk and clean up its tracked
+    /// span. Used when the compiler backtracks.
+    pub fn pop_op(&mut self) {
+        // Simply drop the last op.
+        self.code.pop();
+
+        if let Some(span) = self.spans.last() {
+            // If the last span started at this op, drop it.
+            if span.start == self.code.len() {
+                self.spans.pop();
+            }
+        }
+    }
+
+    pub fn push_constant(&mut self, data: Value) -> ConstantIdx {
+        let idx = self.constants.len();
+        self.constants.push(data);
+        ConstantIdx(idx)
+    }
+
+    // Span tracking implementation
+
+    fn push_span(&mut self, span: codemap::Span, start: usize) {
+        match self.spans.last_mut() {
+            // We do not need to insert the same span again, as this
+            // instruction was compiled from the same span as the last
+            // one.
+            Some(last) if last.span == span => {}
+
+            // In all other cases, this is a new source span.
+            _ => self.spans.push(SourceSpan { span, start }),
+        }
+    }
+
+    /// Retrieve the [codemap::Span] from which the instruction at
+    /// `offset` was compiled.
+    pub fn get_span(&self, offset: CodeIdx) -> codemap::Span {
+        let position = self
+            .spans
+            .binary_search_by(|span| span.start.cmp(&offset.0));
+
+        let span = match position {
+            Ok(index) => &self.spans[index],
+            Err(index) => {
+                if index == 0 {
+                    &self.spans[0]
+                } else {
+                    &self.spans[index - 1]
+                }
+            }
+        };
+
+        span.span
+    }
+
+    /// Write the disassembler representation of the operation at
+    /// `idx` to the specified writer.
+    pub fn disassemble_op<W: Write>(
+        &self,
+        writer: &mut W,
+        source: &SourceCode,
+        width: usize,
+        idx: CodeIdx,
+    ) -> Result<(), std::io::Error> {
+        write!(writer, "{:#width$x}\t ", idx.0, width = width)?;
+
+        // Print continuation character if the previous operation was at
+        // the same line, otherwise print the line.
+        let line = source.get_line(self.get_span(idx));
+        if idx.0 > 0 && source.get_line(self.get_span(CodeIdx(idx.0 - 1))) == line {
+            write!(writer, "   |\t")?;
+        } else {
+            write!(writer, "{:4}\t", line)?;
+        }
+
+        match self[idx] {
+            OpCode::OpConstant(idx) => {
+                let val_str = match &self[idx] {
+                    Value::Thunk(t) => t.debug_repr(),
+                    Value::Closure(c) => format!("closure({:p})", c.lambda),
+                    val => format!("{}", val),
+                };
+
+                writeln!(writer, "OpConstant({}@{})", val_str, idx.0)
+            }
+            op => writeln!(writer, "{:?}", op),
+        }?;
+
+        Ok(())
+    }
+
+    /// Extend this chunk with the content of another, moving out of the other
+    /// in the process.
+    ///
+    /// This is used by the compiler when it detects that it unnecessarily
+    /// thunked a nested expression.
+    pub fn extend(&mut self, other: Self) {
+        // Some operations need to be modified in certain ways before being
+        // valid as part of the new chunk.
+        let const_count = self.constants.len();
+        for (idx, op) in other.code.iter().enumerate() {
+            let span = other.get_span(CodeIdx(idx));
+            match op {
+                // As the constants shift, the index needs to be moved relatively.
+                OpCode::OpConstant(ConstantIdx(idx)) => {
+                    self.push_op(OpCode::OpConstant(ConstantIdx(idx + const_count)), span)
+                }
+
+                // Other operations either operate on relative offsets, or no
+                // offsets, and are safe to keep as-is.
+                _ => self.push_op(*op, span),
+            };
+        }
+
+        self.constants.extend(other.constants);
+        self.spans.extend(other.spans);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::dummy_span;
+
+    // Note: These tests are about the functionality of the `Chunk` type, the
+    // opcodes used below do *not* represent valid, executable Tvix code (and
+    // don't need to).
+
+    #[test]
+    fn push_op() {
+        let mut chunk = Chunk::default();
+        chunk.push_op(OpCode::OpAdd, dummy_span());
+        assert_eq!(chunk.code.last().unwrap(), &OpCode::OpAdd);
+    }
+
+    #[test]
+    fn extend_empty() {
+        let mut chunk = Chunk::default();
+        chunk.push_op(OpCode::OpAdd, dummy_span());
+
+        let other = Chunk::default();
+        chunk.extend(other);
+
+        assert_eq!(
+            chunk.code,
+            vec![OpCode::OpAdd],
+            "code should not have changed"
+        );
+    }
+
+    #[test]
+    fn extend_simple() {
+        let span = dummy_span();
+        let mut chunk = Chunk::default();
+        chunk.push_op(OpCode::OpAdd, span);
+
+        let mut other = Chunk::default();
+        other.push_op(OpCode::OpSub, span);
+        other.push_op(OpCode::OpMul, span);
+
+        let expected_code = vec![OpCode::OpAdd, OpCode::OpSub, OpCode::OpMul];
+
+        chunk.extend(other);
+
+        assert_eq!(chunk.code, expected_code, "code should have been extended");
+    }
+
+    #[test]
+    fn extend_with_constant() {
+        let span = dummy_span();
+        let mut chunk = Chunk::default();
+        chunk.push_op(OpCode::OpAdd, span);
+        let cidx = chunk.push_constant(Value::Integer(0));
+        assert_eq!(
+            cidx.0, 0,
+            "first constant in main chunk should have index 0"
+        );
+        chunk.push_op(OpCode::OpConstant(cidx), span);
+
+        let mut other = Chunk::default();
+        other.push_op(OpCode::OpSub, span);
+        let other_cidx = other.push_constant(Value::Integer(1));
+        assert_eq!(
+            other_cidx.0, 0,
+            "first constant in other chunk should have index 0"
+        );
+        other.push_op(OpCode::OpConstant(other_cidx), span);
+
+        chunk.extend(other);
+
+        let expected_code = vec![
+            OpCode::OpAdd,
+            OpCode::OpConstant(ConstantIdx(0)),
+            OpCode::OpSub,
+            OpCode::OpConstant(ConstantIdx(1)), // <- note: this was rewritten
+        ];
+
+        assert_eq!(
+            chunk.code, expected_code,
+            "code should have been extended and rewritten"
+        );
+
+        assert_eq!(chunk.constants.len(), 2);
+        assert!(matches!(chunk.constants[0], Value::Integer(0)));
+        assert!(matches!(chunk.constants[1], Value::Integer(1)));
+    }
+}
diff --git a/tvix/eval/src/compiler/bindings.rs b/tvix/eval/src/compiler/bindings.rs
new file mode 100644
index 000000000000..a3d7c6fbfb33
--- /dev/null
+++ b/tvix/eval/src/compiler/bindings.rs
@@ -0,0 +1,826 @@
+//! This module implements compiler logic related to name/value binding
+//! definitions (that is, attribute sets and let-expressions).
+//!
+//! In the case of recursive scopes these cases share almost all of their
+//! (fairly complex) logic.
+
+use std::iter::Peekable;
+
+use rnix::ast::HasEntry;
+use rowan::ast::AstChildren;
+
+use super::*;
+
+type PeekableAttrs = Peekable<AstChildren<ast::Attr>>;
+
+/// What kind of bindings scope is being compiled?
+#[derive(Clone, Copy, PartialEq)]
+enum BindingsKind {
+    /// Standard `let ... in ...`-expression.
+    LetIn,
+
+    /// Non-recursive attribute set.
+    Attrs,
+
+    /// Recursive attribute set.
+    RecAttrs,
+}
+
+impl BindingsKind {
+    fn is_attrs(&self) -> bool {
+        matches!(self, BindingsKind::Attrs | BindingsKind::RecAttrs)
+    }
+}
+
+// Internal representation of an attribute set used for merging sets, or
+// inserting nested keys.
+#[derive(Clone)]
+struct AttributeSet {
+    /// Original span at which this set was first encountered.
+    span: Span,
+
+    /// Tracks the kind of set (rec or not).
+    kind: BindingsKind,
+
+    /// All inherited entries
+    inherits: Vec<ast::Inherit>,
+
+    /// All internal entries
+    entries: Vec<(Span, PeekableAttrs, ast::Expr)>,
+}
+
+impl ToSpan for AttributeSet {
+    fn span_for(&self, _: &codemap::File) -> Span {
+        self.span
+    }
+}
+
+impl AttributeSet {
+    fn from_ast(c: &Compiler, node: &ast::AttrSet) -> Self {
+        AttributeSet {
+            span: c.span_for(node),
+
+            // Kind of the attrs depends on the first time it is
+            // encountered. We actually believe this to be a Nix
+            // bug: https://github.com/NixOS/nix/issues/7111
+            kind: if node.rec_token().is_some() {
+                BindingsKind::RecAttrs
+            } else {
+                BindingsKind::Attrs
+            },
+
+            inherits: ast::HasEntry::inherits(node).collect(),
+
+            entries: ast::HasEntry::attrpath_values(node)
+                .map(|entry| {
+                    let span = c.span_for(&entry);
+                    (
+                        span,
+                        entry.attrpath().unwrap().attrs().peekable(),
+                        entry.value().unwrap(),
+                    )
+                })
+                .collect(),
+        }
+    }
+}
+
+// Data structures to track the bindings observed in the second pass, and
+// forward the information needed to compile their value.
+enum Binding {
+    InheritFrom {
+        namespace: ast::Expr,
+        name: SmolStr,
+        span: Span,
+    },
+
+    Plain {
+        expr: ast::Expr,
+    },
+
+    Set(AttributeSet),
+}
+
+impl Binding {
+    /// Merge the provided value into the current binding, or emit an
+    /// error if this turns out to be impossible.
+    fn merge(
+        &mut self,
+        c: &mut Compiler,
+        span: Span,
+        mut remaining_path: PeekableAttrs,
+        value: ast::Expr,
+    ) {
+        match self {
+            Binding::InheritFrom { name, ref span, .. } => {
+                c.emit_error(span, ErrorKind::UnmergeableInherit { name: name.clone() })
+            }
+
+            // If the value is not yet a nested binding, flip the representation
+            // and recurse.
+            Binding::Plain { expr } => match expr {
+                ast::Expr::AttrSet(existing) => {
+                    let nested = AttributeSet::from_ast(c, existing);
+                    *self = Binding::Set(nested);
+                    self.merge(c, span, remaining_path, value);
+                }
+
+                _ => c.emit_error(&value, ErrorKind::UnmergeableValue),
+            },
+
+            // If the value is nested further, it is simply inserted into the
+            // bindings with its full path and resolved recursively further
+            // down.
+            Binding::Set(existing) if remaining_path.peek().is_some() => {
+                existing.entries.push((span, remaining_path, value))
+            }
+
+            Binding::Set(existing) => {
+                if let ast::Expr::AttrSet(new) = value {
+                    existing.inherits.extend(ast::HasEntry::inherits(&new));
+                    existing
+                        .entries
+                        .extend(ast::HasEntry::attrpath_values(&new).map(|entry| {
+                            let span = c.span_for(&entry);
+                            (
+                                span,
+                                entry.attrpath().unwrap().attrs().peekable(),
+                                entry.value().unwrap(),
+                            )
+                        }));
+                } else {
+                    // This branch is unreachable because in cases where the
+                    // path is empty (i.e. there is no further nesting), the
+                    // previous try_merge function already verified that the
+                    // expression is an attribute set.
+
+                    // TODO(tazjin): Consider making this branch live by
+                    // shuffling that check around and emitting a static error
+                    // here instead of a runtime error.
+                    unreachable!()
+                }
+            }
+        }
+    }
+}
+
+enum KeySlot {
+    /// There is no key slot (`let`-expressions do not emit their key).
+    None { name: SmolStr },
+
+    /// The key is statically known and has a slot.
+    Static { slot: LocalIdx, name: SmolStr },
+
+    /// The key is dynamic, i.e. only known at runtime, and must be compiled
+    /// into its slot.
+    Dynamic { slot: LocalIdx, attr: ast::Attr },
+}
+
+struct TrackedBinding {
+    key_slot: KeySlot,
+    value_slot: LocalIdx,
+    binding: Binding,
+}
+
+impl TrackedBinding {
+    /// Does this binding match the given key?
+    ///
+    /// Used to determine which binding to merge another one into.
+    fn matches(&self, key: &str) -> bool {
+        match &self.key_slot {
+            KeySlot::None { name } => name == key,
+            KeySlot::Static { name, .. } => name == key,
+            KeySlot::Dynamic { .. } => false,
+        }
+    }
+}
+
+struct TrackedBindings {
+    bindings: Vec<TrackedBinding>,
+}
+
+impl TrackedBindings {
+    fn new() -> Self {
+        TrackedBindings { bindings: vec![] }
+    }
+
+    /// Attempt to merge an entry into an existing matching binding, assuming
+    /// that the provided binding is mergable (i.e. either a nested key or an
+    /// attribute set literal).
+    ///
+    /// Returns true if the binding was merged, false if it needs to be compiled
+    /// separately as a new binding.
+    fn try_merge(
+        &mut self,
+        c: &mut Compiler,
+        span: Span,
+        name: &ast::Attr,
+        mut remaining_path: PeekableAttrs,
+        value: ast::Expr,
+    ) -> bool {
+        // If the path has no more entries, and if the entry is not an
+        // attribute set literal, the entry can not be merged.
+        if remaining_path.peek().is_none() && !matches!(value, ast::Expr::AttrSet(_)) {
+            return false;
+        }
+
+        // If the first element of the path is not statically known, the entry
+        // can not be merged.
+        let name = match expr_static_attr_str(name) {
+            Some(name) => name,
+            None => return false,
+        };
+
+        // If there is no existing binding with this key, the entry can not be
+        // merged.
+        // TODO: benchmark whether using a map or something is useful over the
+        // `find` here
+        let binding = match self.bindings.iter_mut().find(|b| b.matches(&name)) {
+            Some(b) => b,
+            None => return false,
+        };
+
+        // No more excuses ... the binding can be merged!
+        binding.binding.merge(c, span, remaining_path, value);
+
+        true
+    }
+
+    /// Add a completely new binding to the tracked bindings.
+    fn track_new(&mut self, key_slot: KeySlot, value_slot: LocalIdx, binding: Binding) {
+        self.bindings.push(TrackedBinding {
+            key_slot,
+            value_slot,
+            binding,
+        });
+    }
+}
+
+/// Wrapper around the `ast::HasEntry` trait as that trait can not be
+/// implemented for custom types.
+trait HasEntryProxy {
+    fn inherits(&self) -> Box<dyn Iterator<Item = ast::Inherit>>;
+
+    fn attributes(
+        &self,
+        file: Arc<codemap::File>,
+    ) -> Box<dyn Iterator<Item = (Span, PeekableAttrs, ast::Expr)>>;
+}
+
+impl<N: HasEntry> HasEntryProxy for N {
+    fn inherits(&self) -> Box<dyn Iterator<Item = ast::Inherit>> {
+        Box::new(ast::HasEntry::inherits(self))
+    }
+
+    fn attributes(
+        &self,
+        file: Arc<codemap::File>,
+    ) -> Box<dyn Iterator<Item = (Span, PeekableAttrs, ast::Expr)>> {
+        Box::new(ast::HasEntry::attrpath_values(self).map(move |entry| {
+            (
+                entry.span_for(&file),
+                entry.attrpath().unwrap().attrs().peekable(),
+                entry.value().unwrap(),
+            )
+        }))
+    }
+}
+
+impl HasEntryProxy for AttributeSet {
+    fn inherits(&self) -> Box<dyn Iterator<Item = ast::Inherit>> {
+        Box::new(self.inherits.clone().into_iter())
+    }
+
+    fn attributes(
+        &self,
+        _: Arc<codemap::File>,
+    ) -> Box<dyn Iterator<Item = (Span, PeekableAttrs, ast::Expr)>> {
+        Box::new(self.entries.clone().into_iter())
+    }
+}
+
+/// AST-traversing functions related to bindings.
+impl Compiler<'_> {
+    /// Compile all inherits of a node with entries that do *not* have a
+    /// namespace to inherit from, and return the remaining ones that do.
+    fn compile_plain_inherits<N>(
+        &mut self,
+        slot: LocalIdx,
+        kind: BindingsKind,
+        count: &mut usize,
+        node: &N,
+    ) -> Vec<(ast::Expr, SmolStr, Span)>
+    where
+        N: ToSpan + HasEntryProxy,
+    {
+        // Pass over all inherits, resolving only those without namespaces.
+        // Since they always resolve in a higher scope, we can just compile and
+        // declare them immediately.
+        //
+        // Inherits with namespaces are returned to the caller.
+        let mut inherit_froms: Vec<(ast::Expr, SmolStr, Span)> = vec![];
+
+        for inherit in node.inherits() {
+            if inherit.attrs().peekable().peek().is_none() {
+                self.emit_warning(&inherit, WarningKind::EmptyInherit);
+                continue;
+            }
+
+            match inherit.from() {
+                // Within a `let` binding, inheriting from the outer scope is a
+                // no-op *if* there are no dynamic bindings.
+                None if !kind.is_attrs() && !self.has_dynamic_ancestor() => {
+                    self.emit_warning(&inherit, WarningKind::UselessInherit);
+                    continue;
+                }
+
+                None => {
+                    for attr in inherit.attrs() {
+                        let name = match expr_static_attr_str(&attr) {
+                            Some(name) => name,
+                            None => {
+                                self.emit_error(&attr, ErrorKind::DynamicKeyInScope("inherit"));
+                                continue;
+                            }
+                        };
+
+                        // If the identifier resolves statically in a `let`, it
+                        // has precedence over dynamic bindings, and the inherit
+                        // is useless.
+                        if kind == BindingsKind::LetIn
+                            && matches!(
+                                self.scope_mut().resolve_local(&name),
+                                LocalPosition::Known(_)
+                            )
+                        {
+                            self.emit_warning(&attr, WarningKind::UselessInherit);
+                            continue;
+                        }
+
+                        *count += 1;
+
+                        // Place key on the stack when compiling attribute sets.
+                        if kind.is_attrs() {
+                            self.emit_constant(Value::String(name.as_str().into()), &attr);
+                            let span = self.span_for(&attr);
+                            self.scope_mut().declare_phantom(span, true);
+                        }
+
+                        // Place the value on the stack. Note that because plain
+                        // inherits are always in the outer scope, the slot of
+                        // *this* scope itself is used.
+                        self.compile_identifier_access(slot, &name, &attr);
+
+                        // In non-recursive attribute sets, the key slot must be
+                        // a phantom (i.e. the identifier can not be resolved in
+                        // this scope).
+                        let idx = if kind == BindingsKind::Attrs {
+                            let span = self.span_for(&attr);
+                            self.scope_mut().declare_phantom(span, false)
+                        } else {
+                            self.declare_local(&attr, name)
+                        };
+
+                        self.scope_mut().mark_initialised(idx);
+                    }
+                }
+
+                Some(from) => {
+                    for attr in inherit.attrs() {
+                        let name = match expr_static_attr_str(&attr) {
+                            Some(name) => name,
+                            None => {
+                                self.emit_error(&attr, ErrorKind::DynamicKeyInScope("inherit"));
+                                continue;
+                            }
+                        };
+
+                        *count += 1;
+                        inherit_froms.push((from.expr().unwrap(), name, self.span_for(&attr)));
+                    }
+                }
+            }
+        }
+
+        inherit_froms
+    }
+
+    /// Declare all namespaced inherits, that is inherits which are inheriting
+    /// values from an attribute set.
+    ///
+    /// This only ensures that the locals stack is aware of the inherits, it
+    /// does not yet emit bytecode that places them on the stack. This is up to
+    /// the owner of the `bindings` vector, which this function will populate.
+    fn declare_namespaced_inherits(
+        &mut self,
+        kind: BindingsKind,
+        inherit_froms: Vec<(ast::Expr, SmolStr, Span)>,
+        bindings: &mut TrackedBindings,
+    ) {
+        for (from, name, span) in inherit_froms {
+            let key_slot = if kind.is_attrs() {
+                // In an attribute set, the keys themselves are placed on the
+                // stack but their stack slot is inaccessible (it is only
+                // consumed by `OpAttrs`).
+                KeySlot::Static {
+                    slot: self.scope_mut().declare_phantom(span, false),
+                    name: name.clone(),
+                }
+            } else {
+                KeySlot::None { name: name.clone() }
+            };
+
+            let value_slot = match kind {
+                // In recursive scopes, the value needs to be accessible on the
+                // stack.
+                BindingsKind::LetIn | BindingsKind::RecAttrs => {
+                    self.declare_local(&span, name.clone())
+                }
+
+                // In non-recursive attribute sets, the value is inaccessible
+                // (only consumed by `OpAttrs`).
+                BindingsKind::Attrs => self.scope_mut().declare_phantom(span, false),
+            };
+
+            bindings.track_new(
+                key_slot,
+                value_slot,
+                Binding::InheritFrom {
+                    namespace: from,
+                    name,
+                    span,
+                },
+            );
+        }
+    }
+
+    /// Declare all regular bindings (i.e. `key = value;`) in a bindings scope,
+    /// but do not yet compile their values.
+    fn declare_bindings<N>(
+        &mut self,
+        kind: BindingsKind,
+        count: &mut usize,
+        bindings: &mut TrackedBindings,
+        node: &N,
+    ) where
+        N: ToSpan + HasEntryProxy,
+    {
+        for (span, mut path, value) in node.attributes(self.file.clone()) {
+            let key = path.next().unwrap();
+
+            if bindings.try_merge(self, span, &key, path.clone(), value.clone()) {
+                // Binding is nested, or already exists and was merged, move on.
+                continue;
+            }
+
+            *count += 1;
+
+            let key_span = self.span_for(&key);
+            let key_slot = match expr_static_attr_str(&key) {
+                Some(name) if kind.is_attrs() => KeySlot::Static {
+                    name,
+                    slot: self.scope_mut().declare_phantom(key_span, false),
+                },
+
+                Some(name) => KeySlot::None { name },
+
+                None if kind.is_attrs() => KeySlot::Dynamic {
+                    attr: key,
+                    slot: self.scope_mut().declare_phantom(key_span, false),
+                },
+
+                None => {
+                    self.emit_error(&key, ErrorKind::DynamicKeyInScope("let-expression"));
+                    continue;
+                }
+            };
+
+            let value_slot = match kind {
+                BindingsKind::LetIn | BindingsKind::RecAttrs => match &key_slot {
+                    // In recursive scopes, the value needs to be accessible on the
+                    // stack if it is statically known
+                    KeySlot::None { name } | KeySlot::Static { name, .. } => {
+                        self.declare_local(&key_span, name.as_str())
+                    }
+
+                    // Dynamic values are never resolvable (as their names are
+                    // of course only known at runtime).
+                    //
+                    // Note: This branch is unreachable in `let`-expressions.
+                    KeySlot::Dynamic { .. } => self.scope_mut().declare_phantom(key_span, false),
+                },
+
+                // In non-recursive attribute sets, the value is inaccessible
+                // (only consumed by `OpAttrs`).
+                BindingsKind::Attrs => self.scope_mut().declare_phantom(key_span, false),
+            };
+
+            let binding = if path.peek().is_some() {
+                Binding::Set(AttributeSet {
+                    span,
+                    kind: BindingsKind::Attrs,
+                    inherits: vec![],
+                    entries: vec![(span, path, value)],
+                })
+            } else {
+                Binding::Plain { expr: value }
+            };
+
+            bindings.track_new(key_slot, value_slot, binding);
+        }
+    }
+
+    /// Compile attribute set literals into equivalent bytecode.
+    ///
+    /// This is complicated by a number of features specific to Nix attribute
+    /// sets, most importantly:
+    ///
+    /// 1. Keys can be dynamically constructed through interpolation.
+    /// 2. Keys can refer to nested attribute sets.
+    /// 3. Attribute sets can (optionally) be recursive.
+    pub(super) fn compile_attr_set(&mut self, slot: LocalIdx, node: &ast::AttrSet) {
+        // Open a scope to track the positions of the temporaries used by the
+        // `OpAttrs` instruction.
+        self.scope_mut().begin_scope();
+
+        let kind = if node.rec_token().is_some() {
+            BindingsKind::RecAttrs
+        } else {
+            BindingsKind::Attrs
+        };
+
+        self.compile_bindings(slot, kind, node);
+
+        // Remove the temporary scope, but do not emit any additional cleanup
+        // (OpAttrs consumes all of these locals).
+        self.scope_mut().end_scope();
+    }
+
+    /// Actually binds all tracked bindings by emitting the bytecode that places
+    /// them in their stack slots.
+    fn bind_values(&mut self, bindings: TrackedBindings) {
+        let mut value_indices: Vec<LocalIdx> = vec![];
+
+        for binding in bindings.bindings.into_iter() {
+            value_indices.push(binding.value_slot);
+
+            match binding.key_slot {
+                KeySlot::None { .. } => {} // nothing to do here
+
+                KeySlot::Static { slot, name } => {
+                    let span = self.scope()[slot].span;
+                    self.emit_constant(Value::String(name.as_str().into()), &span);
+                    self.scope_mut().mark_initialised(slot);
+                }
+
+                KeySlot::Dynamic { slot, attr } => {
+                    self.compile_attr(slot, &attr);
+                    self.scope_mut().mark_initialised(slot);
+                }
+            }
+
+            match binding.binding {
+                // This entry is an inherit (from) expr. The value is placed on
+                // the stack by selecting an attribute.
+                Binding::InheritFrom {
+                    namespace,
+                    name,
+                    span,
+                } => {
+                    // Create a thunk wrapping value (which may be one as well)
+                    // to avoid forcing the from expr too early.
+                    self.thunk(binding.value_slot, &namespace, |c, s| {
+                        c.compile(s, namespace.clone());
+                        c.emit_force(&namespace);
+
+                        c.emit_constant(Value::String(name.as_str().into()), &span);
+                        c.push_op(OpCode::OpAttrsSelect, &span);
+                    })
+                }
+
+                // Binding is "just" a plain expression that needs to be
+                // compiled.
+                Binding::Plain { expr } => self.compile(binding.value_slot, expr),
+
+                // Binding is a merged or nested attribute set, and needs to be
+                // recursively compiled as another binding.
+                Binding::Set(set) => self.thunk(binding.value_slot, &set, |c, _| {
+                    c.scope_mut().begin_scope();
+                    c.compile_bindings(binding.value_slot, set.kind, &set);
+                    c.scope_mut().end_scope();
+                }),
+            }
+
+            // Any code after this point will observe the value in the right
+            // stack slot, so mark it as initialised.
+            self.scope_mut().mark_initialised(binding.value_slot);
+        }
+
+        // Final pass to emit finaliser instructions if necessary.
+        for idx in value_indices {
+            if self.scope()[idx].needs_finaliser {
+                let stack_idx = self.scope().stack_index(idx);
+                let span = self.scope()[idx].span;
+                self.push_op(OpCode::OpFinalise(stack_idx), &span);
+            }
+        }
+    }
+
+    fn compile_bindings<N>(&mut self, slot: LocalIdx, kind: BindingsKind, node: &N)
+    where
+        N: ToSpan + HasEntryProxy,
+    {
+        let mut count = 0;
+        self.scope_mut().begin_scope();
+
+        // Vector to track all observed bindings.
+        let mut bindings = TrackedBindings::new();
+
+        let inherit_froms = self.compile_plain_inherits(slot, kind, &mut count, node);
+        self.declare_namespaced_inherits(kind, inherit_froms, &mut bindings);
+        self.declare_bindings(kind, &mut count, &mut bindings, node);
+
+        // Check if we can bail out on empty bindings
+        if count == 0 {
+            // still need an attrset to exist, but it is empty.
+            if kind.is_attrs() {
+                self.emit_constant(Value::Attrs(Box::new(NixAttrs::empty())), node);
+                return;
+            }
+
+            self.emit_warning(node, WarningKind::EmptyLet);
+            return;
+        }
+
+        // Actually bind values and ensure they are on the stack.
+        self.bind_values(bindings);
+
+        if kind.is_attrs() {
+            self.push_op(OpCode::OpAttrs(Count(count)), node);
+        }
+
+        if count == 0 {
+            self.unthunk();
+        }
+    }
+
+    /// Compile a standard `let ...; in ...` expression.
+    ///
+    /// Unless in a non-standard scope, the encountered values are simply pushed
+    /// on the stack and their indices noted in the entries vector.
+    pub(super) fn compile_let_in(&mut self, slot: LocalIdx, node: &ast::LetIn) {
+        self.compile_bindings(slot, BindingsKind::LetIn, node);
+
+        // Deal with the body, then clean up the locals afterwards.
+        self.compile(slot, node.body().unwrap());
+        self.cleanup_scope(node);
+    }
+
+    pub(super) fn compile_legacy_let(&mut self, slot: LocalIdx, node: &ast::LegacyLet) {
+        self.emit_warning(node, WarningKind::DeprecatedLegacyLet);
+        self.scope_mut().begin_scope();
+        self.compile_bindings(slot, BindingsKind::RecAttrs, node);
+
+        // Remove the temporary scope, but do not emit any additional cleanup
+        // (OpAttrs consumes all of these locals).
+        self.scope_mut().end_scope();
+
+        self.emit_constant(Value::String("body".into()), node);
+        self.push_op(OpCode::OpAttrsSelect, node);
+    }
+
+    /// Is the given identifier defined *by the user* in any current scope?
+    pub(super) fn is_user_defined(&mut self, ident: &str) -> bool {
+        matches!(
+            self.scope_mut().resolve_local(ident),
+            LocalPosition::Known(_) | LocalPosition::Recursive(_)
+        )
+    }
+
+    /// Resolve and compile access to an identifier in the scope.
+    fn compile_identifier_access<N: ToSpan + Clone>(
+        &mut self,
+        slot: LocalIdx,
+        ident: &str,
+        node: &N,
+    ) {
+        match self.scope_mut().resolve_local(ident) {
+            LocalPosition::Unknown => {
+                // Are we possibly dealing with an upvalue?
+                if let Some(idx) = self.resolve_upvalue(self.contexts.len() - 1, ident, node) {
+                    self.push_op(OpCode::OpGetUpvalue(idx), node);
+                    return;
+                }
+
+                // Globals are the "upmost upvalues": they behave
+                // exactly like a `let ... in` prepended to the
+                // program's text, and the global scope is nothing
+                // more than the parent scope of the root scope.
+                if let Some(global) = self.globals.get(ident) {
+                    self.emit_constant(global.clone(), &self.span_for(node));
+                    return;
+                }
+
+                // If there is a non-empty `with`-stack (or a parent context
+                // with one), emit a runtime dynamic resolution instruction.
+                //
+                // Since it is possible for users to e.g. assign a variable to a
+                // dynamic resolution without actually using it, this operation
+                // is wrapped in an extra thunk.
+                if self.has_dynamic_ancestor() {
+                    self.thunk(slot, node, |c, _| {
+                        c.context_mut().captures_with_stack = true;
+                        c.emit_constant(Value::String(ident.into()), node);
+                        c.push_op(OpCode::OpResolveWith, node);
+                    });
+                    return;
+                }
+
+                // Otherwise, this variable is missing.
+                self.emit_error(node, ErrorKind::UnknownStaticVariable);
+            }
+
+            LocalPosition::Known(idx) => {
+                let stack_idx = self.scope().stack_index(idx);
+                self.push_op(OpCode::OpGetLocal(stack_idx), node);
+            }
+
+            // This identifier is referring to a value from the same scope which
+            // is not yet defined. This identifier access must be thunked.
+            LocalPosition::Recursive(idx) => self.thunk(slot, node, move |compiler, _| {
+                let upvalue_idx = compiler.add_upvalue(
+                    compiler.contexts.len() - 1,
+                    node,
+                    UpvalueKind::Local(idx),
+                );
+                compiler.push_op(OpCode::OpGetUpvalue(upvalue_idx), node);
+            }),
+        };
+    }
+
+    pub(super) fn compile_ident(&mut self, slot: LocalIdx, node: &ast::Ident) {
+        let ident = node.ident_token().unwrap();
+        self.compile_identifier_access(slot, ident.text(), node);
+    }
+}
+
+/// Private compiler helpers related to bindings.
+impl Compiler<'_> {
+    fn resolve_upvalue<N: ToSpan>(
+        &mut self,
+        ctx_idx: usize,
+        name: &str,
+        node: &N,
+    ) -> Option<UpvalueIdx> {
+        if ctx_idx == 0 {
+            // There can not be any upvalue at the outermost context.
+            return None;
+        }
+
+        // Determine whether the upvalue is a local in the enclosing context.
+        match self.contexts[ctx_idx - 1].scope.resolve_local(name) {
+            // recursive upvalues are dealt with the same way as standard known
+            // ones, as thunks and closures are guaranteed to be placed on the
+            // stack (i.e. in the right position) *during* their runtime
+            // construction
+            LocalPosition::Known(idx) | LocalPosition::Recursive(idx) => {
+                return Some(self.add_upvalue(ctx_idx, node, UpvalueKind::Local(idx)))
+            }
+
+            LocalPosition::Unknown => { /* continue below */ }
+        };
+
+        // If the upvalue comes from even further up, we need to recurse to make
+        // sure that the upvalues are created at each level.
+        if let Some(idx) = self.resolve_upvalue(ctx_idx - 1, name, node) {
+            return Some(self.add_upvalue(ctx_idx, node, UpvalueKind::Upvalue(idx)));
+        }
+
+        None
+    }
+
+    fn add_upvalue<N: ToSpan>(
+        &mut self,
+        ctx_idx: usize,
+        node: &N,
+        kind: UpvalueKind,
+    ) -> UpvalueIdx {
+        // If there is already an upvalue closing over the specified index,
+        // retrieve that instead.
+        for (idx, existing) in self.contexts[ctx_idx].scope.upvalues.iter().enumerate() {
+            if existing.kind == kind {
+                return UpvalueIdx(idx);
+            }
+        }
+
+        let span = self.span_for(node);
+        self.contexts[ctx_idx]
+            .scope
+            .upvalues
+            .push(Upvalue { kind, span });
+
+        let idx = UpvalueIdx(self.contexts[ctx_idx].lambda.upvalue_count);
+        self.contexts[ctx_idx].lambda.upvalue_count += 1;
+        idx
+    }
+}
diff --git a/tvix/eval/src/compiler/import.rs b/tvix/eval/src/compiler/import.rs
new file mode 100644
index 000000000000..7e1aabbf1f8b
--- /dev/null
+++ b/tvix/eval/src/compiler/import.rs
@@ -0,0 +1,116 @@
+//! This module implements the Nix language's `import` feature, which
+//! is exposed as a builtin in the Nix language.
+//!
+//! This is not a typical builtin, as it needs access to internal
+//! compiler and VM state (such as the [`crate::SourceCode`]
+//! instance, or observers).
+
+use super::GlobalsMap;
+use genawaiter::rc::Gen;
+use std::rc::Weak;
+
+use crate::{
+    builtins::coerce_value_to_path,
+    generators::pin_generator,
+    observer::NoOpObserver,
+    value::{Builtin, Thunk},
+    vm::generators::{self, GenCo},
+    ErrorKind, SourceCode, Value,
+};
+
+async fn import_impl(
+    co: GenCo,
+    globals: Weak<GlobalsMap>,
+    source: SourceCode,
+    mut args: Vec<Value>,
+) -> Result<Value, ErrorKind> {
+    let mut path = coerce_value_to_path(&co, args.pop().unwrap()).await?;
+
+    if path.is_dir() {
+        path.push("default.nix");
+    }
+
+    if let Some(cached) = generators::request_import_cache_lookup(&co, path.clone()).await {
+        return Ok(cached);
+    }
+
+    // TODO(tazjin): make this return a string directly instead
+    let contents = generators::request_read_to_string(&co, path.clone())
+        .await
+        .to_str()?
+        .as_str()
+        .to_string();
+
+    let parsed = rnix::ast::Root::parse(&contents);
+    let errors = parsed.errors();
+    let file = source.add_file(path.to_string_lossy().to_string(), contents);
+
+    if !errors.is_empty() {
+        return Err(ErrorKind::ImportParseError {
+            path,
+            file,
+            errors: errors.to_vec(),
+        });
+    }
+
+    let result = crate::compiler::compile(
+        &parsed.tree().expr().unwrap(),
+        Some(path.clone()),
+        file,
+        // The VM must ensure that a strong reference to the globals outlives
+        // any self-references (which are weak) embedded within the globals. If
+        // the expect() below panics, it means that did not happen.
+        globals
+            .upgrade()
+            .expect("globals dropped while still in use"),
+        &mut NoOpObserver::default(),
+    )
+    .map_err(|err| ErrorKind::ImportCompilerError {
+        path: path.clone(),
+        errors: vec![err],
+    })?;
+
+    if !result.errors.is_empty() {
+        return Err(ErrorKind::ImportCompilerError {
+            path,
+            errors: result.errors,
+        });
+    }
+
+    for warning in result.warnings {
+        generators::emit_warning(&co, warning).await;
+    }
+
+    // Compilation succeeded, we can construct a thunk from whatever it spat
+    // out and return that.
+    let res = Value::Thunk(Thunk::new_suspended(
+        result.lambda,
+        generators::request_span(&co).await,
+    ));
+
+    generators::request_import_cache_put(&co, path, res.clone()).await;
+
+    Ok(res)
+}
+
+/// Constructs the `import` builtin. This builtin is special in that
+/// it needs to capture the [crate::SourceCode] structure to correctly
+/// track source code locations while invoking a compiler.
+// TODO: need to be able to pass through a CompilationObserver, too.
+// TODO: can the `SourceCode` come from the compiler?
+pub(super) fn builtins_import(globals: &Weak<GlobalsMap>, source: SourceCode) -> Builtin {
+    // This (very cheap, once-per-compiler-startup) clone exists
+    // solely in order to keep the borrow checker happy.  It
+    // resolves the tension between the requirements of
+    // Rc::new_cyclic() and Builtin::new()
+    let globals = globals.clone();
+
+    Builtin::new(
+        "import",
+        Some("Import the given file and return the Nix value it evaluates to"),
+        1,
+        move |args| {
+            Gen::new(|co| pin_generator(import_impl(co, globals.clone(), source.clone(), args)))
+        },
+    )
+}
diff --git a/tvix/eval/src/compiler/mod.rs b/tvix/eval/src/compiler/mod.rs
new file mode 100644
index 000000000000..478d132758e8
--- /dev/null
+++ b/tvix/eval/src/compiler/mod.rs
@@ -0,0 +1,1497 @@
+//! This module implements a compiler for compiling the rnix AST
+//! representation to Tvix bytecode.
+//!
+//! A note on `unwrap()`: This module contains a lot of calls to
+//! `unwrap()` or `expect(...)` on data structures returned by `rnix`.
+//! The reason for this is that rnix uses the same data structures to
+//! represent broken and correct ASTs, so all typed AST variants have
+//! the ability to represent an incorrect node.
+//!
+//! However, at the time that the AST is passed to the compiler we
+//! have verified that `rnix` considers the code to be correct, so all
+//! variants are fulfilled. In cases where the invariant is guaranteed
+//! by the code in this module, `debug_assert!` has been used to catch
+//! mistakes early during development.
+
+mod bindings;
+mod import;
+mod optimiser;
+mod scope;
+
+use codemap::Span;
+use rnix::ast::{self, AstToken};
+use smol_str::SmolStr;
+use std::collections::HashMap;
+use std::path::{Path, PathBuf};
+use std::rc::{Rc, Weak};
+use std::sync::Arc;
+
+use crate::chunk::Chunk;
+use crate::errors::{Error, ErrorKind, EvalResult};
+use crate::observer::CompilerObserver;
+use crate::opcode::{CodeIdx, ConstantIdx, Count, JumpOffset, OpCode, UpvalueIdx};
+use crate::spans::LightSpan;
+use crate::spans::ToSpan;
+use crate::value::{Closure, Formals, Lambda, NixAttrs, Thunk, Value};
+use crate::warnings::{EvalWarning, WarningKind};
+use crate::SourceCode;
+
+use self::scope::{LocalIdx, LocalPosition, Scope, Upvalue, UpvalueKind};
+
+/// Represents the result of compiling a piece of Nix code. If
+/// compilation was successful, the resulting bytecode can be passed
+/// to the VM.
+pub struct CompilationOutput {
+    pub lambda: Rc<Lambda>,
+    pub warnings: Vec<EvalWarning>,
+    pub errors: Vec<Error>,
+
+    // This field must outlive the rc::Weak reference which breaks the
+    // builtins -> import -> builtins reference cycle. For this
+    // reason, it must be passed to the VM.
+    pub globals: Rc<GlobalsMap>,
+}
+
+/// Represents the lambda currently being compiled.
+struct LambdaCtx {
+    lambda: Lambda,
+    scope: Scope,
+    captures_with_stack: bool,
+    unthunk: bool,
+}
+
+impl LambdaCtx {
+    fn new() -> Self {
+        LambdaCtx {
+            lambda: Lambda::default(),
+            scope: Default::default(),
+            captures_with_stack: false,
+            unthunk: false,
+        }
+    }
+
+    fn inherit(&self) -> Self {
+        LambdaCtx {
+            lambda: Lambda::default(),
+            scope: self.scope.inherit(),
+            captures_with_stack: false,
+            unthunk: false,
+        }
+    }
+}
+
+/// The map of globally available functions and other values that
+/// should implicitly be resolvable in the global scope.
+pub(crate) type GlobalsMap = HashMap<&'static str, Value>;
+
+/// Set of builtins that (if they exist) should be made available in
+/// the global scope, meaning that they can be accessed not just
+/// through `builtins.<name>`, but directly as `<name>`. This is not
+/// configurable, it is based on what Nix 2.3 exposed.
+const GLOBAL_BUILTINS: &[&str] = &[
+    "abort",
+    "baseNameOf",
+    "derivation",
+    "derivationStrict",
+    "dirOf",
+    "fetchGit",
+    "fetchMercurial",
+    "fetchTarball",
+    "fromTOML",
+    "import",
+    "isNull",
+    "map",
+    "placeholder",
+    "removeAttrs",
+    "scopedImport",
+    "throw",
+    "toString",
+];
+
+pub struct Compiler<'observer> {
+    contexts: Vec<LambdaCtx>,
+    warnings: Vec<EvalWarning>,
+    errors: Vec<Error>,
+    root_dir: PathBuf,
+
+    /// Carries all known global tokens; the full set of which is
+    /// created when the compiler is invoked.
+    ///
+    /// Each global has an associated token, which when encountered as
+    /// an identifier is resolved against the scope poisoning logic,
+    /// and a function that should emit code for the token.
+    globals: Rc<GlobalsMap>,
+
+    /// File reference in the codemap contains all known source code
+    /// and is used to track the spans from which instructions where
+    /// derived.
+    file: Arc<codemap::File>,
+
+    /// Carry an observer for the compilation process, which is called
+    /// whenever a chunk is emitted.
+    observer: &'observer mut dyn CompilerObserver,
+
+    /// Carry a count of nested scopes which have requested the
+    /// compiler not to emit anything. This used for compiling dead
+    /// code branches to catch errors & warnings in them.
+    dead_scope: usize,
+}
+
+impl Compiler<'_> {
+    pub(super) fn span_for<S: ToSpan>(&self, to_span: &S) -> Span {
+        to_span.span_for(&self.file)
+    }
+}
+
+/// Compiler construction
+impl<'observer> Compiler<'observer> {
+    pub(crate) fn new(
+        location: Option<PathBuf>,
+        file: Arc<codemap::File>,
+        globals: Rc<GlobalsMap>,
+        observer: &'observer mut dyn CompilerObserver,
+    ) -> EvalResult<Self> {
+        let mut root_dir = match location {
+            Some(dir) if cfg!(target_arch = "wasm32") || dir.is_absolute() => Ok(dir),
+            _ => {
+                let current_dir = std::env::current_dir().map_err(|e| {
+                    Error::new(
+                        ErrorKind::RelativePathResolution(format!(
+                            "could not determine current directory: {}",
+                            e
+                        )),
+                        file.span,
+                    )
+                })?;
+                if let Some(dir) = location {
+                    Ok(current_dir.join(dir))
+                } else {
+                    Ok(current_dir)
+                }
+            }
+        }?;
+
+        // If the path passed from the caller points to a file, the
+        // filename itself needs to be truncated as this must point to a
+        // directory.
+        if root_dir.is_file() {
+            root_dir.pop();
+        }
+
+        #[cfg(not(target_arch = "wasm32"))]
+        debug_assert!(root_dir.is_absolute());
+
+        Ok(Self {
+            root_dir,
+            file,
+            observer,
+            globals,
+            contexts: vec![LambdaCtx::new()],
+            warnings: vec![],
+            errors: vec![],
+            dead_scope: 0,
+        })
+    }
+}
+
+// Helper functions for emitting code and metadata to the internal
+// structures of the compiler.
+impl Compiler<'_> {
+    fn context(&self) -> &LambdaCtx {
+        &self.contexts[self.contexts.len() - 1]
+    }
+
+    fn context_mut(&mut self) -> &mut LambdaCtx {
+        let idx = self.contexts.len() - 1;
+        &mut self.contexts[idx]
+    }
+
+    fn chunk(&mut self) -> &mut Chunk {
+        &mut self.context_mut().lambda.chunk
+    }
+
+    fn scope(&self) -> &Scope {
+        &self.context().scope
+    }
+
+    fn scope_mut(&mut self) -> &mut Scope {
+        &mut self.context_mut().scope
+    }
+
+    /// Push a single instruction to the current bytecode chunk and
+    /// track the source span from which it was compiled.
+    fn push_op<T: ToSpan>(&mut self, data: OpCode, node: &T) -> CodeIdx {
+        if self.dead_scope > 0 {
+            return CodeIdx(0);
+        }
+
+        let span = self.span_for(node);
+        self.chunk().push_op(data, span)
+    }
+
+    /// Emit a single constant to the current bytecode chunk and track
+    /// the source span from which it was compiled.
+    pub(super) fn emit_constant<T: ToSpan>(&mut self, value: Value, node: &T) {
+        if self.dead_scope > 0 {
+            return;
+        }
+
+        let idx = self.chunk().push_constant(value);
+        self.push_op(OpCode::OpConstant(idx), node);
+    }
+}
+
+// Actual code-emitting AST traversal methods.
+impl Compiler<'_> {
+    fn compile(&mut self, slot: LocalIdx, expr: ast::Expr) {
+        let expr = optimiser::optimise_expr(self, slot, expr);
+
+        match &expr {
+            ast::Expr::Literal(literal) => self.compile_literal(literal),
+            ast::Expr::Path(path) => self.compile_path(slot, path),
+            ast::Expr::Str(s) => self.compile_str(slot, s),
+
+            ast::Expr::UnaryOp(op) => self.thunk(slot, op, move |c, s| c.compile_unary_op(s, op)),
+
+            ast::Expr::BinOp(binop) => {
+                self.thunk(slot, binop, move |c, s| c.compile_binop(s, binop))
+            }
+
+            ast::Expr::HasAttr(has_attr) => {
+                self.thunk(slot, has_attr, move |c, s| c.compile_has_attr(s, has_attr))
+            }
+
+            ast::Expr::List(list) => self.thunk(slot, list, move |c, s| c.compile_list(s, list)),
+
+            ast::Expr::AttrSet(attrs) => {
+                self.thunk(slot, attrs, move |c, s| c.compile_attr_set(s, attrs))
+            }
+
+            ast::Expr::Select(select) => {
+                self.thunk(slot, select, move |c, s| c.compile_select(s, select))
+            }
+
+            ast::Expr::Assert(assert) => {
+                self.thunk(slot, assert, move |c, s| c.compile_assert(s, assert))
+            }
+            ast::Expr::IfElse(if_else) => {
+                self.thunk(slot, if_else, move |c, s| c.compile_if_else(s, if_else))
+            }
+
+            ast::Expr::LetIn(let_in) => {
+                self.thunk(slot, let_in, move |c, s| c.compile_let_in(s, let_in))
+            }
+
+            ast::Expr::Ident(ident) => self.compile_ident(slot, ident),
+            ast::Expr::With(with) => self.thunk(slot, with, |c, s| c.compile_with(s, with)),
+            ast::Expr::Lambda(lambda) => self.thunk(slot, lambda, move |c, s| {
+                c.compile_lambda_or_thunk(false, s, lambda, |c, s| c.compile_lambda(s, lambda))
+            }),
+            ast::Expr::Apply(apply) => {
+                self.thunk(slot, apply, move |c, s| c.compile_apply(s, apply))
+            }
+
+            // Parenthesized expressions are simply unwrapped, leaving
+            // their value on the stack.
+            ast::Expr::Paren(paren) => self.compile(slot, paren.expr().unwrap()),
+
+            ast::Expr::LegacyLet(legacy_let) => self.thunk(slot, legacy_let, move |c, s| {
+                c.compile_legacy_let(s, legacy_let)
+            }),
+
+            ast::Expr::Root(_) => unreachable!("there cannot be more than one root"),
+            ast::Expr::Error(_) => unreachable!("compile is only called on validated trees"),
+        }
+    }
+
+    /// Compiles an expression, but does not emit any code for it as
+    /// it is considered dead. This will still catch errors and
+    /// warnings in that expression.
+    ///
+    /// A warning about the that code being dead is assumed to already be
+    /// emitted by the caller of [compile_dead_code].
+    fn compile_dead_code(&mut self, slot: LocalIdx, node: ast::Expr) {
+        self.dead_scope += 1;
+        self.compile(slot, node);
+        self.dead_scope -= 1;
+    }
+
+    fn compile_literal(&mut self, node: &ast::Literal) {
+        let value = match node.kind() {
+            ast::LiteralKind::Float(f) => Value::Float(f.value().unwrap()),
+            ast::LiteralKind::Integer(i) => match i.value() {
+                Ok(v) => Value::Integer(v),
+                Err(err) => return self.emit_error(node, err.into()),
+            },
+
+            ast::LiteralKind::Uri(u) => {
+                self.emit_warning(node, WarningKind::DeprecatedLiteralURL);
+                Value::String(u.syntax().text().into())
+            }
+        };
+
+        self.emit_constant(value, node);
+    }
+
+    fn compile_path(&mut self, slot: LocalIdx, node: &ast::Path) {
+        // TODO(tazjin): placeholder implementation while waiting for
+        // https://github.com/nix-community/rnix-parser/pull/96
+
+        let raw_path = node.to_string();
+        let path = if raw_path.starts_with('/') {
+            Path::new(&raw_path).to_owned()
+        } else if raw_path.starts_with('~') {
+            return self.thunk(slot, node, move |c, _| {
+                // We assume that home paths start with ~/ or fail to parse
+                // TODO: this should be checked using a parse-fail test.
+                debug_assert!(raw_path.len() > 2 && raw_path.starts_with("~/"));
+
+                let home_relative_path = &raw_path[2..(raw_path.len())];
+                c.emit_constant(
+                    Value::UnresolvedPath(Box::new(home_relative_path.into())),
+                    node,
+                );
+                c.push_op(OpCode::OpResolveHomePath, node);
+            });
+        } else if raw_path.starts_with('<') {
+            // TODO: decide what to do with findFile
+            if raw_path.len() == 2 {
+                return self.emit_error(
+                    node,
+                    ErrorKind::NixPathResolution("Empty <> path not allowed".into()),
+                );
+            }
+            let path = &raw_path[1..(raw_path.len() - 1)];
+            // Make a thunk to resolve the path (without using `findFile`, at least for now?)
+            return self.thunk(slot, node, move |c, _| {
+                c.emit_constant(Value::UnresolvedPath(Box::new(path.into())), node);
+                c.push_op(OpCode::OpFindFile, node);
+            });
+        } else {
+            let mut buf = self.root_dir.clone();
+            buf.push(&raw_path);
+            buf
+        };
+
+        // TODO: Use https://github.com/rust-lang/rfcs/issues/2208
+        // once it is available
+        let value = Value::Path(Box::new(crate::value::canon_path(path)));
+        self.emit_constant(value, node);
+    }
+
+    /// Helper that compiles the given string parts strictly. The caller
+    /// (`compile_str`) needs to figure out if the result of compiling this
+    /// needs to be thunked or not.
+    fn compile_str_parts(
+        &mut self,
+        slot: LocalIdx,
+        parent_node: &ast::Str,
+        parts: Vec<ast::InterpolPart<String>>,
+    ) {
+        // The string parts are produced in literal order, however
+        // they need to be reversed on the stack in order to
+        // efficiently create the real string in case of
+        // interpolation.
+        for part in parts.iter().rev() {
+            match part {
+                // Interpolated expressions are compiled as normal and
+                // dealt with by the VM before being assembled into
+                // the final string. We need to coerce them here,
+                // so OpInterpolate definitely has a string to consume.
+                ast::InterpolPart::Interpolation(ipol) => {
+                    self.compile(slot, ipol.expr().unwrap());
+                    // implicitly forces as well
+                    self.push_op(OpCode::OpCoerceToString, ipol);
+                }
+
+                ast::InterpolPart::Literal(lit) => {
+                    self.emit_constant(Value::String(lit.as_str().into()), parent_node);
+                }
+            }
+        }
+
+        if parts.len() != 1 {
+            self.push_op(OpCode::OpInterpolate(Count(parts.len())), parent_node);
+        }
+    }
+
+    fn compile_str(&mut self, slot: LocalIdx, node: &ast::Str) {
+        let parts = node.normalized_parts();
+
+        // We need to thunk string expressions if they are the result of
+        // interpolation. A string that only consists of a single part (`"${foo}"`)
+        // can't desugar to the enclosed expression (`foo`) because we need to
+        // coerce the result to a string value. This would require forcing the
+        // value of the inner expression, so we need to wrap it in another thunk.
+        if parts.len() != 1 || matches!(&parts[0], ast::InterpolPart::Interpolation(_)) {
+            self.thunk(slot, node, move |c, s| {
+                c.compile_str_parts(s, node, parts);
+            });
+        } else {
+            self.compile_str_parts(slot, node, parts);
+        }
+    }
+
+    fn compile_unary_op(&mut self, slot: LocalIdx, op: &ast::UnaryOp) {
+        self.compile(slot, op.expr().unwrap());
+        self.emit_force(op);
+
+        let opcode = match op.operator().unwrap() {
+            ast::UnaryOpKind::Invert => OpCode::OpInvert,
+            ast::UnaryOpKind::Negate => OpCode::OpNegate,
+        };
+
+        self.push_op(opcode, op);
+    }
+
+    fn compile_binop(&mut self, slot: LocalIdx, op: &ast::BinOp) {
+        use ast::BinOpKind;
+
+        // Short-circuiting and other strange operators, which are
+        // under the same node type as NODE_BIN_OP, but need to be
+        // handled separately (i.e. before compiling the expressions
+        // used for standard binary operators).
+
+        match op.operator().unwrap() {
+            BinOpKind::And => return self.compile_and(slot, op),
+            BinOpKind::Or => return self.compile_or(slot, op),
+            BinOpKind::Implication => return self.compile_implication(slot, op),
+            _ => {}
+        };
+
+        // For all other operators, the two values need to be left on
+        // the stack in the correct order before pushing the
+        // instruction for the operation itself.
+        self.compile(slot, op.lhs().unwrap());
+        self.emit_force(&op.lhs().unwrap());
+
+        self.compile(slot, op.rhs().unwrap());
+        self.emit_force(&op.rhs().unwrap());
+
+        match op.operator().unwrap() {
+            BinOpKind::Add => self.push_op(OpCode::OpAdd, op),
+            BinOpKind::Sub => self.push_op(OpCode::OpSub, op),
+            BinOpKind::Mul => self.push_op(OpCode::OpMul, op),
+            BinOpKind::Div => self.push_op(OpCode::OpDiv, op),
+            BinOpKind::Update => self.push_op(OpCode::OpAttrsUpdate, op),
+            BinOpKind::Equal => self.push_op(OpCode::OpEqual, op),
+            BinOpKind::Less => self.push_op(OpCode::OpLess, op),
+            BinOpKind::LessOrEq => self.push_op(OpCode::OpLessOrEq, op),
+            BinOpKind::More => self.push_op(OpCode::OpMore, op),
+            BinOpKind::MoreOrEq => self.push_op(OpCode::OpMoreOrEq, op),
+            BinOpKind::Concat => self.push_op(OpCode::OpConcat, op),
+
+            BinOpKind::NotEqual => {
+                self.push_op(OpCode::OpEqual, op);
+                self.push_op(OpCode::OpInvert, op)
+            }
+
+            // Handled by separate branch above.
+            BinOpKind::And | BinOpKind::Implication | BinOpKind::Or => {
+                unreachable!()
+            }
+        };
+    }
+
+    fn compile_and(&mut self, slot: LocalIdx, node: &ast::BinOp) {
+        debug_assert!(
+            matches!(node.operator(), Some(ast::BinOpKind::And)),
+            "compile_and called with wrong operator kind: {:?}",
+            node.operator(),
+        );
+
+        // Leave left-hand side value on the stack.
+        self.compile(slot, node.lhs().unwrap());
+        self.emit_force(&node.lhs().unwrap());
+
+        // If this value is false, jump over the right-hand side - the
+        // whole expression is false.
+        let end_idx = self.push_op(OpCode::OpJumpIfFalse(JumpOffset(0)), node);
+
+        // Otherwise, remove the previous value and leave the
+        // right-hand side on the stack. Its result is now the value
+        // of the whole expression.
+        self.push_op(OpCode::OpPop, node);
+        self.compile(slot, node.rhs().unwrap());
+        self.emit_force(&node.rhs().unwrap());
+
+        self.patch_jump(end_idx);
+        self.push_op(OpCode::OpAssertBool, node);
+    }
+
+    fn compile_or(&mut self, slot: LocalIdx, node: &ast::BinOp) {
+        debug_assert!(
+            matches!(node.operator(), Some(ast::BinOpKind::Or)),
+            "compile_or called with wrong operator kind: {:?}",
+            node.operator(),
+        );
+
+        // Leave left-hand side value on the stack
+        self.compile(slot, node.lhs().unwrap());
+        self.emit_force(&node.lhs().unwrap());
+
+        // Opposite of above: If this value is **true**, we can
+        // short-circuit the right-hand side.
+        let end_idx = self.push_op(OpCode::OpJumpIfTrue(JumpOffset(0)), node);
+        self.push_op(OpCode::OpPop, node);
+        self.compile(slot, node.rhs().unwrap());
+        self.emit_force(&node.rhs().unwrap());
+
+        self.patch_jump(end_idx);
+        self.push_op(OpCode::OpAssertBool, node);
+    }
+
+    fn compile_implication(&mut self, slot: LocalIdx, node: &ast::BinOp) {
+        debug_assert!(
+            matches!(node.operator(), Some(ast::BinOpKind::Implication)),
+            "compile_implication called with wrong operator kind: {:?}",
+            node.operator(),
+        );
+
+        // Leave left-hand side value on the stack and invert it.
+        self.compile(slot, node.lhs().unwrap());
+        self.emit_force(&node.lhs().unwrap());
+        self.push_op(OpCode::OpInvert, node);
+
+        // Exactly as `||` (because `a -> b` = `!a || b`).
+        let end_idx = self.push_op(OpCode::OpJumpIfTrue(JumpOffset(0)), node);
+        self.push_op(OpCode::OpPop, node);
+        self.compile(slot, node.rhs().unwrap());
+        self.emit_force(&node.rhs().unwrap());
+
+        self.patch_jump(end_idx);
+        self.push_op(OpCode::OpAssertBool, node);
+    }
+
+    /// Compile list literals into equivalent bytecode. List
+    /// construction is fairly simple, consisting of pushing code for
+    /// each literal element and an instruction with the element
+    /// count.
+    ///
+    /// The VM, after evaluating the code for each element, simply
+    /// constructs the list from the given number of elements.
+    fn compile_list(&mut self, slot: LocalIdx, node: &ast::List) {
+        let mut count = 0;
+
+        // Open a temporary scope to correctly account for stack items
+        // that exist during the construction.
+        self.scope_mut().begin_scope();
+
+        for item in node.items() {
+            // Start tracing new stack slots from the second list
+            // element onwards. The first list element is located in
+            // the stack slot of the list itself.
+            let item_slot = match count {
+                0 => slot,
+                _ => {
+                    let item_span = self.span_for(&item);
+                    self.scope_mut().declare_phantom(item_span, false)
+                }
+            };
+
+            count += 1;
+            self.compile(item_slot, item);
+            self.scope_mut().mark_initialised(item_slot);
+        }
+
+        if count == 0 {
+            self.unthunk();
+        }
+
+        self.push_op(OpCode::OpList(Count(count)), node);
+        self.scope_mut().end_scope();
+    }
+
+    fn compile_attr(&mut self, slot: LocalIdx, node: &ast::Attr) {
+        match node {
+            ast::Attr::Dynamic(dynamic) => {
+                self.compile(slot, dynamic.expr().unwrap());
+                self.emit_force(&dynamic.expr().unwrap());
+            }
+
+            ast::Attr::Str(s) => {
+                self.compile_str(slot, s);
+                self.emit_force(s);
+            }
+
+            ast::Attr::Ident(ident) => self.emit_literal_ident(ident),
+        }
+    }
+
+    fn compile_has_attr(&mut self, slot: LocalIdx, node: &ast::HasAttr) {
+        // Put the attribute set on the stack.
+        self.compile(slot, node.expr().unwrap());
+        self.emit_force(node);
+
+        // Push all path fragments with an operation for fetching the
+        // next nested element, for all fragments except the last one.
+        for (count, fragment) in node.attrpath().unwrap().attrs().enumerate() {
+            if count > 0 {
+                self.push_op(OpCode::OpAttrsTrySelect, &fragment);
+                self.emit_force(&fragment);
+            }
+
+            self.compile_attr(slot, &fragment);
+        }
+
+        // After the last fragment, emit the actual instruction that
+        // leaves a boolean on the stack.
+        self.push_op(OpCode::OpHasAttr, node);
+    }
+
+    /// When compiling select or select_or expressions, an optimisation is
+    /// possible of compiling the set emitted a constant attribute set by
+    /// immediately replacing it with the actual value.
+    ///
+    /// We take care not to emit an error here, as that would interfere with
+    /// thunking behaviour (there can be perfectly valid Nix code that accesses
+    /// a statically known attribute set that is lacking a key, because that
+    /// thunk is never evaluated). If anything is missing, just inform the
+    /// caller that the optimisation did not take place and move on. We may want
+    /// to emit warnings here in the future.
+    fn optimise_select(&mut self, path: &ast::Attrpath) -> bool {
+        // If compiling the set emitted a constant attribute set, the
+        // associated constant can immediately be replaced with the
+        // actual value.
+        //
+        // We take care not to emit an error here, as that would
+        // interfere with thunking behaviour (there can be perfectly
+        // valid Nix code that accesses a statically known attribute
+        // set that is lacking a key, because that thunk is never
+        // evaluated). If anything is missing, just move on. We may
+        // want to emit warnings here in the future.
+        if let Some(OpCode::OpConstant(ConstantIdx(idx))) = self.chunk().code.last().cloned() {
+            let constant = &mut self.chunk().constants[idx];
+            if let Value::Attrs(attrs) = constant {
+                let mut path_iter = path.attrs();
+
+                // Only do this optimisation if there is a *single*
+                // element in the attribute path. It is extremely
+                // unlikely that we'd have a static nested set.
+                if let (Some(attr), None) = (path_iter.next(), path_iter.next()) {
+                    // Only do this optimisation for statically known attrs.
+                    if let Some(ident) = expr_static_attr_str(&attr) {
+                        if let Some(selected_value) = attrs.select(ident.as_str()) {
+                            *constant = selected_value.clone();
+
+                            // If this worked, we can unthunk the current thunk.
+                            self.unthunk();
+
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+
+        false
+    }
+
+    fn compile_select(&mut self, slot: LocalIdx, node: &ast::Select) {
+        let set = node.expr().unwrap();
+        let path = node.attrpath().unwrap();
+
+        if node.or_token().is_some() {
+            return self.compile_select_or(slot, set, path, node.default_expr().unwrap());
+        }
+
+        // Push the set onto the stack
+        self.compile(slot, set.clone());
+        if self.optimise_select(&path) {
+            return;
+        }
+
+        // Compile each key fragment and emit access instructions.
+        //
+        // TODO: multi-select instruction to avoid re-pushing attrs on
+        // nested selects.
+        for fragment in path.attrs() {
+            // Force the current set value.
+            self.emit_force(&set);
+
+            self.compile_attr(slot, &fragment);
+            self.push_op(OpCode::OpAttrsSelect, &fragment);
+        }
+    }
+
+    /// Compile an `or` expression into a chunk of conditional jumps.
+    ///
+    /// If at any point during attribute set traversal a key is
+    /// missing, the `OpAttrOrNotFound` instruction will leave a
+    /// special sentinel value on the stack.
+    ///
+    /// After each access, a conditional jump evaluates the top of the
+    /// stack and short-circuits to the default value if it sees the
+    /// sentinel.
+    ///
+    /// Code like `{ a.b = 1; }.a.c or 42` yields this bytecode and
+    /// runtime stack:
+    ///
+    /// ```notrust
+    ///            Bytecode                     Runtime stack
+    ///  ┌────────────────────────────┐   ┌─────────────────────────┐
+    ///  │    ...                     │   │ ...                     │
+    ///  │ 5  OP_ATTRS(1)             │ → │ 5  [ { a.b = 1; }     ] │
+    ///  │ 6  OP_CONSTANT("a")        │ → │ 6  [ { a.b = 1; } "a" ] │
+    ///  │ 7  OP_ATTR_OR_NOT_FOUND    │ → │ 7  [ { b = 1; }       ] │
+    ///  │ 8  JUMP_IF_NOT_FOUND(13)   │ → │ 8  [ { b = 1; }       ] │
+    ///  │ 9  OP_CONSTANT("C")        │ → │ 9  [ { b = 1; } "c"   ] │
+    ///  │ 10 OP_ATTR_OR_NOT_FOUND    │ → │ 10 [ NOT_FOUND        ] │
+    ///  │ 11 JUMP_IF_NOT_FOUND(13)   │ → │ 11 [                  ] │
+    ///  │ 12 JUMP(14)                │   │ ..     jumped over      │
+    ///  │ 13 CONSTANT(42)            │ → │ 12 [ 42 ]               │
+    ///  │ 14 ...                     │   │ ..   ....               │
+    ///  └────────────────────────────┘   └─────────────────────────┘
+    /// ```
+    fn compile_select_or(
+        &mut self,
+        slot: LocalIdx,
+        set: ast::Expr,
+        path: ast::Attrpath,
+        default: ast::Expr,
+    ) {
+        self.compile(slot, set);
+        if self.optimise_select(&path) {
+            return;
+        }
+
+        let mut jumps = vec![];
+
+        for fragment in path.attrs() {
+            self.emit_force(&fragment);
+            self.compile_attr(slot, &fragment.clone());
+            self.push_op(OpCode::OpAttrsTrySelect, &fragment);
+            jumps.push(self.push_op(OpCode::OpJumpIfNotFound(JumpOffset(0)), &fragment));
+        }
+
+        let final_jump = self.push_op(OpCode::OpJump(JumpOffset(0)), &path);
+
+        for jump in jumps {
+            self.patch_jump(jump);
+        }
+
+        // Compile the default value expression and patch the final
+        // jump to point *beyond* it.
+        self.compile(slot, default);
+        self.patch_jump(final_jump);
+    }
+
+    /// Compile `assert` expressions using jumping instructions in the VM.
+    ///
+    /// ```notrust
+    ///                        ┌─────────────────────┐
+    ///                        │ 0  [ conditional ]  │
+    ///                        │ 1   JUMP_IF_FALSE  →┼─┐
+    ///                        │ 2  [  main body  ]  │ │ Jump to else body if
+    ///                       ┌┼─3─←     JUMP        │ │ condition is false.
+    ///  Jump over else body  ││ 4   OP_ASSERT_FAIL ←┼─┘
+    ///  if condition is true.└┼─5─→     ...         │
+    ///                        └─────────────────────┘
+    /// ```
+    fn compile_assert(&mut self, slot: LocalIdx, node: &ast::Assert) {
+        // Compile the assertion condition to leave its value on the stack.
+        self.compile(slot, node.condition().unwrap());
+        self.emit_force(&node.condition().unwrap());
+        let then_idx = self.push_op(OpCode::OpJumpIfFalse(JumpOffset(0)), node);
+
+        self.push_op(OpCode::OpPop, node);
+        self.compile(slot, node.body().unwrap());
+
+        let else_idx = self.push_op(OpCode::OpJump(JumpOffset(0)), node);
+
+        self.patch_jump(then_idx);
+        self.push_op(OpCode::OpPop, node);
+        self.push_op(OpCode::OpAssertFail, &node.condition().unwrap());
+
+        self.patch_jump(else_idx);
+    }
+
+    /// Compile conditional expressions using jumping instructions in the VM.
+    ///
+    /// ```notrust
+    ///                        ┌────────────────────┐
+    ///                        │ 0  [ conditional ] │
+    ///                        │ 1   JUMP_IF_FALSE →┼─┐
+    ///                        │ 2  [  main body  ] │ │ Jump to else body if
+    ///                       ┌┼─3─←     JUMP       │ │ condition is false.
+    ///  Jump over else body  ││ 4  [  else body  ]←┼─┘
+    ///  if condition is true.└┼─5─→     ...        │
+    ///                        └────────────────────┘
+    /// ```
+    fn compile_if_else(&mut self, slot: LocalIdx, node: &ast::IfElse) {
+        self.compile(slot, node.condition().unwrap());
+        self.emit_force(&node.condition().unwrap());
+
+        let then_idx = self.push_op(
+            OpCode::OpJumpIfFalse(JumpOffset(0)),
+            &node.condition().unwrap(),
+        );
+
+        self.push_op(OpCode::OpPop, node); // discard condition value
+        self.compile(slot, node.body().unwrap());
+
+        let else_idx = self.push_op(OpCode::OpJump(JumpOffset(0)), node);
+
+        self.patch_jump(then_idx); // patch jump *to* else_body
+        self.push_op(OpCode::OpPop, node); // discard condition value
+        self.compile(slot, node.else_body().unwrap());
+
+        self.patch_jump(else_idx); // patch jump *over* else body
+    }
+
+    /// Compile `with` expressions by emitting instructions that
+    /// pop/remove the indices of attribute sets that are implicitly
+    /// in scope through `with` on the "with-stack".
+    fn compile_with(&mut self, slot: LocalIdx, node: &ast::With) {
+        self.scope_mut().begin_scope();
+        // TODO: Detect if the namespace is just an identifier, and
+        // resolve that directly (thus avoiding duplication on the
+        // stack).
+        self.compile(slot, node.namespace().unwrap());
+
+        let span = self.span_for(&node.namespace().unwrap());
+
+        // The attribute set from which `with` inherits values
+        // occupies a slot on the stack, but this stack slot is not
+        // directly accessible. As it must be accounted for to
+        // calculate correct offsets, what we call a "phantom" local
+        // is declared here.
+        let local_idx = self.scope_mut().declare_phantom(span, true);
+        let with_idx = self.scope().stack_index(local_idx);
+
+        self.scope_mut().push_with();
+
+        self.push_op(OpCode::OpPushWith(with_idx), &node.namespace().unwrap());
+
+        self.compile(slot, node.body().unwrap());
+
+        self.push_op(OpCode::OpPopWith, node);
+        self.scope_mut().pop_with();
+        self.cleanup_scope(node);
+    }
+
+    /// Compiles pattern function arguments, such as `{ a, b }: ...`.
+    ///
+    /// These patterns are treated as a special case of locals binding
+    /// where the attribute set itself is placed on the first stack
+    /// slot of the call frame (either as a phantom, or named in case
+    /// of an `@` binding), and the function call sets up the rest of
+    /// the stack as if the parameters were rewritten into a `let`
+    /// binding.
+    ///
+    /// For example:
+    ///
+    /// ```nix
+    /// ({ a, b ? 2, c ? a * b, ... }@args: <body>)  { a = 10; }
+    /// ```
+    ///
+    /// would be compiled similarly to a binding such as
+    ///
+    /// ```nix
+    /// let args = { a = 10; };
+    /// in let a = args.a;
+    ///        b = args.a or 2;
+    ///        c = args.c or a * b;
+    ///    in <body>
+    /// ```
+    ///
+    /// The only tricky bit being that bindings have to fail if too
+    /// many arguments are provided. This is done by emitting a
+    /// special instruction that checks the set of keys from a
+    /// constant containing the expected keys.
+    fn compile_param_pattern(&mut self, pattern: &ast::Pattern) -> Formals {
+        let span = self.span_for(pattern);
+        let set_idx = match pattern.pat_bind() {
+            Some(name) => self.declare_local(&name, name.ident().unwrap().to_string()),
+            None => self.scope_mut().declare_phantom(span, true),
+        };
+
+        // At call time, the attribute set is already at the top of
+        // the stack.
+        self.scope_mut().mark_initialised(set_idx);
+        self.emit_force(pattern);
+
+        let ellipsis = pattern.ellipsis_token().is_some();
+        if !ellipsis {
+            self.push_op(OpCode::OpValidateClosedFormals, pattern);
+        }
+
+        // Similar to `let ... in ...`, we now do multiple passes over
+        // the bindings to first declare them, then populate them, and
+        // then finalise any necessary recursion into the scope.
+        let mut entries: Vec<(LocalIdx, ast::PatEntry)> = vec![];
+        let mut indices: Vec<LocalIdx> = vec![];
+        let mut arguments = HashMap::default();
+
+        for entry in pattern.pat_entries() {
+            let ident = entry.ident().unwrap();
+            let idx = self.declare_local(&ident, ident.to_string());
+            let has_default = entry.default().is_some();
+            entries.push((idx, entry));
+            indices.push(idx);
+            arguments.insert(ident.into(), has_default);
+        }
+
+        // For each of the bindings, push the set on the stack and
+        // attempt to select from it.
+        let stack_idx = self.scope().stack_index(set_idx);
+        for (idx, entry) in entries.into_iter() {
+            self.push_op(OpCode::OpGetLocal(stack_idx), pattern);
+            self.emit_literal_ident(&entry.ident().unwrap());
+
+            // Use the same mechanism as `compile_select_or` if a
+            // default value was provided, or simply select otherwise.
+            if let Some(default_expr) = entry.default() {
+                self.push_op(OpCode::OpAttrsTrySelect, &entry.ident().unwrap());
+
+                let jump_to_default =
+                    self.push_op(OpCode::OpJumpIfNotFound(JumpOffset(0)), &default_expr);
+
+                let jump_over_default = self.push_op(OpCode::OpJump(JumpOffset(0)), &default_expr);
+
+                self.patch_jump(jump_to_default);
+
+                // Thunk the default expression, but only if it is something
+                // other than an identifier.
+                if let ast::Expr::Ident(_) = &default_expr {
+                    self.compile(idx, default_expr);
+                } else {
+                    self.thunk(idx, &self.span_for(&default_expr), move |c, s| {
+                        c.compile(s, default_expr)
+                    });
+                }
+
+                self.patch_jump(jump_over_default);
+            } else {
+                self.push_op(OpCode::OpAttrsSelect, &entry.ident().unwrap());
+            }
+
+            self.scope_mut().mark_initialised(idx);
+        }
+
+        for idx in indices {
+            if self.scope()[idx].needs_finaliser {
+                let stack_idx = self.scope().stack_index(idx);
+                self.push_op(OpCode::OpFinalise(stack_idx), pattern);
+            }
+        }
+
+        Formals {
+            arguments,
+            ellipsis,
+            span,
+        }
+    }
+
+    fn compile_lambda(&mut self, slot: LocalIdx, node: &ast::Lambda) {
+        // Compile the function itself, recording its formal arguments (if any)
+        // for later use
+        let formals = match node.param().unwrap() {
+            ast::Param::Pattern(pat) => Some(self.compile_param_pattern(&pat)),
+
+            ast::Param::IdentParam(param) => {
+                let name = param
+                    .ident()
+                    .unwrap()
+                    .ident_token()
+                    .unwrap()
+                    .text()
+                    .to_string();
+
+                let idx = self.declare_local(&param, &name);
+                self.scope_mut().mark_initialised(idx);
+                None
+            }
+        };
+
+        self.compile(slot, node.body().unwrap());
+        self.context_mut().lambda.formals = formals;
+    }
+
+    fn thunk<N, F>(&mut self, outer_slot: LocalIdx, node: &N, content: F)
+    where
+        N: ToSpan,
+        F: FnOnce(&mut Compiler, LocalIdx),
+    {
+        self.compile_lambda_or_thunk(true, outer_slot, node, content)
+    }
+
+    /// Mark the current thunk as redundant, i.e. possible to merge directly
+    /// into its parent lambda context without affecting runtime behaviour.
+    fn unthunk(&mut self) {
+        self.context_mut().unthunk = true;
+    }
+
+    /// Compile an expression into a runtime closure or thunk
+    fn compile_lambda_or_thunk<N, F>(
+        &mut self,
+        is_suspended_thunk: bool,
+        outer_slot: LocalIdx,
+        node: &N,
+        content: F,
+    ) where
+        N: ToSpan,
+        F: FnOnce(&mut Compiler, LocalIdx),
+    {
+        let name = self.scope()[outer_slot].name();
+        self.new_context();
+
+        // Set the (optional) name of the current slot on the lambda that is
+        // being compiled.
+        self.context_mut().lambda.name = name;
+
+        let span = self.span_for(node);
+        let slot = self.scope_mut().declare_phantom(span, false);
+        self.scope_mut().begin_scope();
+
+        content(self, slot);
+        self.cleanup_scope(node);
+
+        // TODO: determine and insert enclosing name, if available.
+
+        // Pop the lambda context back off, and emit the finished
+        // lambda as a constant.
+        let mut compiled = self.contexts.pop().unwrap();
+
+        // The compiler might have decided to unthunk, i.e. raise the compiled
+        // code to the parent context. In that case we do so and return right
+        // away.
+        if compiled.unthunk && is_suspended_thunk {
+            self.chunk().extend(compiled.lambda.chunk);
+            return;
+        }
+
+        // Emit an instruction to inform the VM that the chunk has ended.
+        compiled
+            .lambda
+            .chunk
+            .push_op(OpCode::OpReturn, self.span_for(node));
+
+        // Capturing the with stack counts as an upvalue, as it is
+        // emitted as an upvalue data instruction.
+        if compiled.captures_with_stack {
+            compiled.lambda.upvalue_count += 1;
+        }
+
+        let lambda = Rc::new(compiled.lambda);
+        if is_suspended_thunk {
+            self.observer.observe_compiled_thunk(&lambda);
+        } else {
+            self.observer.observe_compiled_lambda(&lambda);
+        }
+
+        // If no upvalues are captured, emit directly and move on.
+        if lambda.upvalue_count == 0 {
+            self.emit_constant(
+                if is_suspended_thunk {
+                    Value::Thunk(Thunk::new_suspended(lambda, LightSpan::new_actual(span)))
+                } else {
+                    Value::Closure(Rc::new(Closure::new(lambda)))
+                },
+                node,
+            );
+            return;
+        }
+
+        // Otherwise, we need to emit the variable number of
+        // operands that allow the runtime to close over the
+        // upvalues and leave a blueprint in the constant index from
+        // which the result can be constructed.
+        let blueprint_idx = self.chunk().push_constant(Value::Blueprint(lambda));
+
+        let code_idx = self.push_op(
+            if is_suspended_thunk {
+                OpCode::OpThunkSuspended(blueprint_idx)
+            } else {
+                OpCode::OpThunkClosure(blueprint_idx)
+            },
+            node,
+        );
+
+        self.emit_upvalue_data(
+            outer_slot,
+            node,
+            compiled.scope.upvalues,
+            compiled.captures_with_stack,
+        );
+
+        if !is_suspended_thunk && !self.scope()[outer_slot].needs_finaliser {
+            if !self.scope()[outer_slot].must_thunk {
+                // The closure has upvalues, but is not recursive.  Therefore no thunk is required,
+                // which saves us the overhead of Rc<RefCell<>>
+                self.chunk()[code_idx] = OpCode::OpClosure(blueprint_idx);
+            } else {
+                // This case occurs when a closure has upvalue-references to itself but does not need a
+                // finaliser.  Since no OpFinalise will be emitted later on we synthesize one here.
+                // It is needed here only to set [`Closure::is_finalised`] which is used for sanity checks.
+                #[cfg(debug_assertions)]
+                self.push_op(
+                    OpCode::OpFinalise(self.scope().stack_index(outer_slot)),
+                    &self.span_for(node),
+                );
+            }
+        }
+    }
+
+    fn compile_apply(&mut self, slot: LocalIdx, node: &ast::Apply) {
+        // To call a function, we leave its arguments on the stack,
+        // followed by the function expression itself, and then emit a
+        // call instruction. This way, the stack is perfectly laid out
+        // to enter the function call straight away.
+        self.compile(slot, node.argument().unwrap());
+        self.compile(slot, node.lambda().unwrap());
+        self.emit_force(&node.lambda().unwrap());
+        self.push_op(OpCode::OpCall, node);
+    }
+
+    /// Emit the data instructions that the runtime needs to correctly
+    /// assemble the upvalues struct.
+    fn emit_upvalue_data<T: ToSpan>(
+        &mut self,
+        slot: LocalIdx,
+        node: &T,
+        upvalues: Vec<Upvalue>,
+        capture_with: bool,
+    ) {
+        for upvalue in upvalues {
+            match upvalue.kind {
+                UpvalueKind::Local(idx) => {
+                    let target = &self.scope()[idx];
+                    let stack_idx = self.scope().stack_index(idx);
+
+                    // If the target is not yet initialised, we need to defer
+                    // the local access
+                    if !target.initialised {
+                        self.push_op(OpCode::DataDeferredLocal(stack_idx), &upvalue.span);
+                        self.scope_mut().mark_needs_finaliser(slot);
+                    } else {
+                        // a self-reference
+                        if slot == idx {
+                            self.scope_mut().mark_must_thunk(slot);
+                        }
+                        self.push_op(OpCode::DataStackIdx(stack_idx), &upvalue.span);
+                    }
+                }
+
+                UpvalueKind::Upvalue(idx) => {
+                    self.push_op(OpCode::DataUpvalueIdx(idx), &upvalue.span);
+                }
+            };
+        }
+
+        if capture_with {
+            // TODO(tazjin): probably better to emit span for the ident that caused this
+            self.push_op(OpCode::DataCaptureWith, node);
+        }
+    }
+
+    /// Emit the literal string value of an identifier. Required for
+    /// several operations related to attribute sets, where
+    /// identifiers are used as string keys.
+    fn emit_literal_ident(&mut self, ident: &ast::Ident) {
+        self.emit_constant(Value::String(ident.clone().into()), ident);
+    }
+
+    /// Patch the jump instruction at the given index, setting its
+    /// jump offset from the placeholder to the current code position.
+    ///
+    /// This is required because the actual target offset of jumps is
+    /// not known at the time when the jump operation itself is
+    /// emitted.
+    fn patch_jump(&mut self, idx: CodeIdx) {
+        let offset = JumpOffset(self.chunk().code.len() - 1 - idx.0);
+
+        match &mut self.chunk().code[idx.0] {
+            OpCode::OpJump(n)
+            | OpCode::OpJumpIfFalse(n)
+            | OpCode::OpJumpIfTrue(n)
+            | OpCode::OpJumpIfNotFound(n) => {
+                *n = offset;
+            }
+
+            op => panic!("attempted to patch unsupported op: {:?}", op),
+        }
+    }
+
+    /// Decrease scope depth of the current function and emit
+    /// instructions to clean up the stack at runtime.
+    fn cleanup_scope<N: ToSpan>(&mut self, node: &N) {
+        // When ending a scope, all corresponding locals need to be
+        // removed, but the value of the body needs to remain on the
+        // stack. This is implemented by a separate instruction.
+        let (popcount, unused_spans) = self.scope_mut().end_scope();
+
+        for span in &unused_spans {
+            self.emit_warning(span, WarningKind::UnusedBinding);
+        }
+
+        if popcount > 0 {
+            self.push_op(OpCode::OpCloseScope(Count(popcount)), node);
+        }
+    }
+
+    /// Open a new lambda context within which to compile a function,
+    /// closure or thunk.
+    fn new_context(&mut self) {
+        self.contexts.push(self.context().inherit());
+    }
+
+    /// Declare a local variable known in the scope that is being
+    /// compiled by pushing it to the locals. This is used to
+    /// determine the stack offset of variables.
+    fn declare_local<S: Into<String>, N: ToSpan>(&mut self, node: &N, name: S) -> LocalIdx {
+        let name = name.into();
+        let depth = self.scope().scope_depth();
+
+        // Do this little dance to turn name:&'a str into the same
+        // string with &'static lifetime, as required by WarningKind
+        if let Some((global_ident, _)) = self.globals.get_key_value(name.as_str()) {
+            self.emit_warning(node, WarningKind::ShadowedGlobal(global_ident));
+        }
+
+        let span = self.span_for(node);
+        let (idx, shadowed) = self.scope_mut().declare_local(name, span);
+
+        if let Some(shadow_idx) = shadowed {
+            let other = &self.scope()[shadow_idx];
+            if other.depth == depth {
+                self.emit_error(node, ErrorKind::VariableAlreadyDefined(other.span));
+            }
+        }
+
+        idx
+    }
+
+    /// Determine whether the current lambda context has any ancestors
+    /// that use dynamic scope resolution, and mark contexts as
+    /// needing to capture their enclosing `with`-stack in their
+    /// upvalues.
+    fn has_dynamic_ancestor(&mut self) -> bool {
+        let mut ancestor_has_with = false;
+
+        for ctx in self.contexts.iter_mut() {
+            if ancestor_has_with {
+                // If the ancestor has an active with stack, mark this
+                // lambda context as needing to capture it.
+                ctx.captures_with_stack = true;
+            } else {
+                // otherwise, check this context and move on
+                ancestor_has_with = ctx.scope.has_with();
+            }
+        }
+
+        ancestor_has_with
+    }
+
+    fn emit_force<N: ToSpan>(&mut self, node: &N) {
+        self.push_op(OpCode::OpForce, node);
+    }
+
+    fn emit_warning<N: ToSpan>(&mut self, node: &N, kind: WarningKind) {
+        let span = self.span_for(node);
+        self.warnings.push(EvalWarning { kind, span })
+    }
+
+    fn emit_error<N: ToSpan>(&mut self, node: &N, kind: ErrorKind) {
+        let span = self.span_for(node);
+        self.errors.push(Error::new(kind, span))
+    }
+}
+
+/// Convert a non-dynamic string expression to a string if possible.
+fn expr_static_str(node: &ast::Str) -> Option<SmolStr> {
+    let mut parts = node.normalized_parts();
+
+    if parts.len() != 1 {
+        return None;
+    }
+
+    if let Some(ast::InterpolPart::Literal(lit)) = parts.pop() {
+        return Some(SmolStr::new(lit));
+    }
+
+    None
+}
+
+/// Convert the provided `ast::Attr` into a statically known string if
+/// possible.
+fn expr_static_attr_str(node: &ast::Attr) -> Option<SmolStr> {
+    match node {
+        ast::Attr::Ident(ident) => Some(ident.ident_token().unwrap().text().into()),
+        ast::Attr::Str(s) => expr_static_str(s),
+
+        // The dynamic node type is just a wrapper. C++ Nix does not care
+        // about the dynamic wrapper when determining whether the node
+        // itself is dynamic, it depends solely on the expression inside
+        // (i.e. `let ${"a"} = 1; in a` is valid).
+        ast::Attr::Dynamic(ref dynamic) => match dynamic.expr().unwrap() {
+            ast::Expr::Str(s) => expr_static_str(&s),
+            _ => None,
+        },
+    }
+}
+
+/// Create a delayed source-only builtin compilation, for a builtin
+/// which is written in Nix code.
+///
+/// **Important:** tvix *panics* if a builtin with invalid source code
+/// is supplied. This is because there is no user-friendly way to
+/// thread the errors out of this function right now.
+fn compile_src_builtin(
+    name: &'static str,
+    code: &str,
+    source: &SourceCode,
+    weak: &Weak<GlobalsMap>,
+) -> Value {
+    use std::fmt::Write;
+
+    let parsed = rnix::ast::Root::parse(code);
+
+    if !parsed.errors().is_empty() {
+        let mut out = format!("BUG: code for source-builtin '{}' had parser errors", name);
+        for error in parsed.errors() {
+            writeln!(out, "{}", error).unwrap();
+        }
+
+        panic!("{}", out);
+    }
+
+    let file = source.add_file(format!("<src-builtins/{}.nix>", name), code.to_string());
+    let weak = weak.clone();
+
+    Value::Thunk(Thunk::new_suspended_native(Box::new(move || {
+        let result = compile(
+            &parsed.tree().expr().unwrap(),
+            None,
+            file.clone(),
+            weak.upgrade().unwrap(),
+            &mut crate::observer::NoOpObserver {},
+        )
+        .map_err(|e| ErrorKind::NativeError {
+            gen_type: "derivation",
+            err: Box::new(e),
+        })?;
+
+        if !result.errors.is_empty() {
+            return Err(ErrorKind::ImportCompilerError {
+                path: format!("src-builtins/{}.nix", name).into(),
+                errors: result.errors,
+            });
+        }
+
+        Ok(Value::Thunk(Thunk::new_suspended(
+            result.lambda,
+            LightSpan::Actual { span: file.span },
+        )))
+    })))
+}
+
+/// Prepare the full set of globals available in evaluated code. These
+/// are constructed from the set of builtins supplied by the caller,
+/// which are made available globally under the `builtins` identifier.
+///
+/// A subset of builtins (specified by [`GLOBAL_BUILTINS`]) is
+/// available globally *iff* they are set.
+///
+/// Optionally adds the `import` feature if desired by the caller.
+pub fn prepare_globals(
+    builtins: Vec<(&'static str, Value)>,
+    src_builtins: Vec<(&'static str, &'static str)>,
+    source: SourceCode,
+    enable_import: bool,
+) -> Rc<GlobalsMap> {
+    Rc::new_cyclic(Box::new(move |weak: &Weak<GlobalsMap>| {
+        // First step is to construct the builtins themselves as
+        // `NixAttrs`.
+        let mut builtins: GlobalsMap = HashMap::from_iter(builtins.into_iter());
+
+        // At this point, optionally insert `import` if enabled. To
+        // "tie the knot" of `import` needing the full set of globals
+        // to instantiate its compiler, the `Weak` reference is passed
+        // here.
+        if enable_import {
+            let import = Value::Builtin(import::builtins_import(weak, source.clone()));
+            builtins.insert("import", import);
+        }
+
+        // Next, the actual map of globals which the compiler will use
+        // to resolve identifiers is constructed.
+        let mut globals: GlobalsMap = HashMap::new();
+
+        // builtins contain themselves (`builtins.builtins`), which we
+        // can resolve by manually constructing a suspended thunk that
+        // dereferences the same weak pointer as above.
+        let weak_globals = weak.clone();
+        builtins.insert(
+            "builtins",
+            Value::Thunk(Thunk::new_suspended_native(Box::new(move || {
+                Ok(weak_globals
+                    .upgrade()
+                    .unwrap()
+                    .get("builtins")
+                    .cloned()
+                    .unwrap())
+            }))),
+        );
+
+        // Insert top-level static value builtins.
+        globals.insert("true", Value::Bool(true));
+        globals.insert("false", Value::Bool(false));
+        globals.insert("null", Value::Null);
+
+        // If "source builtins" were supplied, compile them and insert
+        // them.
+        builtins.extend(src_builtins.into_iter().map(move |(name, code)| {
+            let compiled = compile_src_builtin(name, code, &source, weak);
+            (name, compiled)
+        }));
+
+        // Construct the actual `builtins` attribute set and insert it
+        // in the global scope.
+        globals.insert(
+            "builtins",
+            Value::attrs(NixAttrs::from_iter(builtins.clone().into_iter())),
+        );
+
+        // Finally, the builtins that should be globally available are
+        // "elevated" to the outer scope.
+        for global in GLOBAL_BUILTINS {
+            if let Some(builtin) = builtins.get(global).cloned() {
+                globals.insert(global, builtin);
+            }
+        }
+
+        globals
+    }))
+}
+
+pub fn compile(
+    expr: &ast::Expr,
+    location: Option<PathBuf>,
+    file: Arc<codemap::File>,
+    globals: Rc<GlobalsMap>,
+    observer: &mut dyn CompilerObserver,
+) -> EvalResult<CompilationOutput> {
+    let mut c = Compiler::new(location, file, globals.clone(), observer)?;
+
+    let root_span = c.span_for(expr);
+    let root_slot = c.scope_mut().declare_phantom(root_span, false);
+    c.compile(root_slot, expr.clone());
+
+    // The final operation of any top-level Nix program must always be
+    // `OpForce`. A thunk should not be returned to the user in an
+    // unevaluated state (though in practice, a value *containing* a
+    // thunk might be returned).
+    c.emit_force(expr);
+    c.push_op(OpCode::OpReturn, &root_span);
+
+    let lambda = Rc::new(c.contexts.pop().unwrap().lambda);
+    c.observer.observe_compiled_toplevel(&lambda);
+
+    Ok(CompilationOutput {
+        lambda,
+        warnings: c.warnings,
+        errors: c.errors,
+        globals,
+    })
+}
diff --git a/tvix/eval/src/compiler/optimiser.rs b/tvix/eval/src/compiler/optimiser.rs
new file mode 100644
index 000000000000..48960d355cc6
--- /dev/null
+++ b/tvix/eval/src/compiler/optimiser.rs
@@ -0,0 +1,125 @@
+//! Helper functions for extending the compiler with more linter-like
+//! functionality while compiling (i.e. smarter warnings).
+
+use super::*;
+
+use ast::Expr;
+
+/// Optimise the given expression where possible.
+pub(super) fn optimise_expr(c: &mut Compiler, slot: LocalIdx, expr: ast::Expr) -> ast::Expr {
+    match expr {
+        Expr::BinOp(_) => optimise_bin_op(c, slot, expr),
+        _ => expr,
+    }
+}
+
+enum LitBool {
+    Expr(Expr),
+    True(Expr),
+    False(Expr),
+}
+
+/// Is this a literal boolean, or something else?
+fn is_lit_bool(expr: ast::Expr) -> LitBool {
+    if let ast::Expr::Ident(ident) = &expr {
+        match ident.ident_token().unwrap().text() {
+            "true" => LitBool::True(expr),
+            "false" => LitBool::False(expr),
+            _ => LitBool::Expr(expr),
+        }
+    } else {
+        LitBool::Expr(expr)
+    }
+}
+
+/// Detect useless binary operations (i.e. useless bool comparisons).
+fn optimise_bin_op(c: &mut Compiler, slot: LocalIdx, expr: ast::Expr) -> ast::Expr {
+    use ast::BinOpKind;
+
+    // bail out of this check if the user has overridden either `true`
+    // or `false` identifiers. Note that they will have received a
+    // separate warning about this for shadowing the global(s).
+    if c.is_user_defined("true") || c.is_user_defined("false") {
+        return expr;
+    }
+
+    if let Expr::BinOp(op) = &expr {
+        let lhs = is_lit_bool(op.lhs().unwrap());
+        let rhs = is_lit_bool(op.rhs().unwrap());
+
+        match (op.operator().unwrap(), lhs, rhs) {
+            // useless `false` arm in `||` expression
+            (BinOpKind::Or, LitBool::False(f), LitBool::Expr(other))
+            | (BinOpKind::Or, LitBool::Expr(other), LitBool::False(f)) => {
+                c.emit_warning(
+                    &f,
+                    WarningKind::UselessBoolOperation(
+                        "this `false` has no effect on the result of the comparison",
+                    ),
+                );
+
+                return other;
+            }
+
+            // useless `true` arm in `&&` expression
+            (BinOpKind::And, LitBool::True(t), LitBool::Expr(other))
+            | (BinOpKind::And, LitBool::Expr(other), LitBool::True(t)) => {
+                c.emit_warning(
+                    &t,
+                    WarningKind::UselessBoolOperation(
+                        "this `true` has no effect on the result of the comparison",
+                    ),
+                );
+
+                return other;
+            }
+
+            // useless `||` expression (one arm is `true`), return
+            // `true` directly (and warn about dead code on the right)
+            (BinOpKind::Or, LitBool::True(t), LitBool::Expr(other)) => {
+                c.emit_warning(
+                    op,
+                    WarningKind::UselessBoolOperation("this expression is always true"),
+                );
+
+                c.compile_dead_code(slot, other);
+
+                return t;
+            }
+
+            (BinOpKind::Or, _, LitBool::True(t)) | (BinOpKind::Or, LitBool::True(t), _) => {
+                c.emit_warning(
+                    op,
+                    WarningKind::UselessBoolOperation("this expression is always true"),
+                );
+
+                return t;
+            }
+
+            // useless `&&` expression (one arm is `false), same as above
+            (BinOpKind::And, LitBool::False(f), LitBool::Expr(other)) => {
+                c.emit_warning(
+                    op,
+                    WarningKind::UselessBoolOperation("this expression is always false"),
+                );
+
+                c.compile_dead_code(slot, other);
+
+                return f;
+            }
+
+            (BinOpKind::And, _, LitBool::False(f)) | (BinOpKind::Or, LitBool::False(f), _) => {
+                c.emit_warning(
+                    op,
+                    WarningKind::UselessBoolOperation("this expression is always false"),
+                );
+
+                return f;
+            }
+
+            _ => { /* nothing to optimise */ }
+        }
+    }
+
+    expr
+}
diff --git a/tvix/eval/src/compiler/scope.rs b/tvix/eval/src/compiler/scope.rs
new file mode 100644
index 000000000000..892727c107c9
--- /dev/null
+++ b/tvix/eval/src/compiler/scope.rs
@@ -0,0 +1,378 @@
+//! This module implements the scope-tracking logic of the Tvix
+//! compiler.
+//!
+//! Scoping in Nix is fairly complicated, there are features like
+//! mutually recursive bindings, `with`, upvalue capturing, and so
+//! on that introduce a fair bit of complexity.
+//!
+//! Tvix attempts to do as much of the heavy lifting of this at
+//! compile time, and leave the runtime to mostly deal with known
+//! stack indices. To do this, the compiler simulates where locals
+//! will be at runtime using the data structures implemented here.
+
+use std::{
+    collections::{hash_map, HashMap},
+    ops::Index,
+};
+
+use smol_str::SmolStr;
+
+use crate::opcode::{StackIdx, UpvalueIdx};
+
+#[derive(Debug)]
+enum LocalName {
+    /// Normally declared local with a statically known name.
+    Ident(String),
+
+    /// Phantom stack value (e.g. attribute set used for `with`) that
+    /// must be accounted for to calculate correct stack offsets.
+    Phantom,
+}
+
+/// Represents a single local already known to the compiler.
+#[derive(Debug)]
+pub struct Local {
+    /// Identifier of this local. This is always a statically known
+    /// value (Nix does not allow dynamic identifier names in locals),
+    /// or a "phantom" value not accessible by users.
+    name: LocalName,
+
+    /// Source span at which this local was declared.
+    pub span: codemap::Span,
+
+    /// Scope depth of this local.
+    pub depth: usize,
+
+    /// Is this local initialised?
+    pub initialised: bool,
+
+    /// Is this local known to have been used at all?
+    pub used: bool,
+
+    /// Does this local need to be finalised after the enclosing scope
+    /// is completely constructed?
+    pub needs_finaliser: bool,
+
+    /// Does this local's upvalues contain a reference to itself?
+    pub must_thunk: bool,
+}
+
+impl Local {
+    /// Retrieve the name of the given local (if available).
+    pub fn name(&self) -> Option<SmolStr> {
+        match &self.name {
+            LocalName::Phantom => None,
+            LocalName::Ident(name) => Some(SmolStr::new(name)),
+        }
+    }
+
+    /// Is this local intentionally ignored? (i.e. name starts with `_`)
+    pub fn is_ignored(&self) -> bool {
+        match &self.name {
+            LocalName::Ident(name) => name.starts_with('_'),
+            LocalName::Phantom => false,
+        }
+    }
+}
+
+/// Represents the current position of an identifier as resolved in a scope.
+pub enum LocalPosition {
+    /// Local is not known in this scope.
+    Unknown,
+
+    /// Local is known at the given local index.
+    Known(LocalIdx),
+
+    /// Local is known, but is being accessed recursively within its
+    /// own initialisation. Depending on context, this is either an
+    /// error or forcing a closure/thunk.
+    Recursive(LocalIdx),
+}
+
+/// Represents the different ways in which upvalues can be captured in
+/// closures or thunks.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum UpvalueKind {
+    /// This upvalue captures a local from the stack.
+    Local(LocalIdx),
+
+    /// This upvalue captures an enclosing upvalue.
+    Upvalue(UpvalueIdx),
+}
+
+#[derive(Clone, Debug)]
+pub struct Upvalue {
+    pub kind: UpvalueKind,
+    pub span: codemap::Span,
+}
+
+/// The index of a local in the scope's local array at compile time.
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
+pub struct LocalIdx(usize);
+
+/// Helper struct for indexing over `Scope::locals` by name.
+#[derive(Debug)]
+enum ByName {
+    Single(LocalIdx),
+    Shadowed(Vec<LocalIdx>),
+}
+
+impl ByName {
+    /// Add an additional index for this name.
+    fn add_idx(&mut self, new: LocalIdx) {
+        match self {
+            ByName::Shadowed(indices) => indices.push(new),
+            ByName::Single(idx) => {
+                *self = ByName::Shadowed(vec![*idx, new]);
+            }
+        }
+    }
+
+    /// Remove the most recent index for this name, unless it is a
+    /// single. Returns `true` if an entry was removed.
+    fn remove_idx(&mut self) -> bool {
+        match self {
+            ByName::Single(_) => false,
+            ByName::Shadowed(indices) => match indices[..] {
+                [fst, _snd] => {
+                    *self = ByName::Single(fst);
+                    true
+                }
+                _ => {
+                    indices.pop();
+                    true
+                }
+            },
+        }
+    }
+
+    /// Return the most recent index.
+    pub fn index(&self) -> LocalIdx {
+        match self {
+            ByName::Single(idx) => *idx,
+            ByName::Shadowed(vec) => *vec.last().unwrap(),
+        }
+    }
+}
+
+/// Represents a scope known during compilation, which can be resolved
+/// directly to stack indices.
+#[derive(Debug, Default)]
+pub struct Scope {
+    locals: Vec<Local>,
+    pub upvalues: Vec<Upvalue>,
+
+    /// Secondary by-name index over locals.
+    by_name: HashMap<String, ByName>,
+
+    /// How many scopes "deep" are these locals?
+    scope_depth: usize,
+
+    /// Current size of the `with`-stack at runtime.
+    with_stack_size: usize,
+}
+
+impl Index<LocalIdx> for Scope {
+    type Output = Local;
+
+    fn index(&self, index: LocalIdx) -> &Self::Output {
+        &self.locals[index.0]
+    }
+}
+
+impl Scope {
+    /// Inherit scope details from a parent scope (required for
+    /// correctly nesting scopes in lambdas and thunks when special
+    /// scope features like dynamic resolution are present).
+    pub fn inherit(&self) -> Self {
+        Self {
+            scope_depth: self.scope_depth + 1,
+            with_stack_size: self.with_stack_size,
+            ..Default::default()
+        }
+    }
+
+    /// Increase the `with`-stack size of this scope.
+    pub fn push_with(&mut self) {
+        self.with_stack_size += 1;
+    }
+
+    /// Decrease the `with`-stack size of this scope.
+    pub fn pop_with(&mut self) {
+        self.with_stack_size -= 1;
+    }
+
+    /// Does this scope currently require dynamic runtime resolution
+    /// of identifiers that could not be found?
+    pub fn has_with(&self) -> bool {
+        self.with_stack_size > 0
+    }
+
+    /// Resolve the stack index of a statically known local.
+    pub fn resolve_local(&mut self, name: &str) -> LocalPosition {
+        if let Some(by_name) = self.by_name.get(name) {
+            let idx = by_name.index();
+            let local = self
+                .locals
+                .get_mut(idx.0)
+                .expect("invalid compiler state: indexed local missing");
+
+            local.used = true;
+
+            // This local is still being initialised, meaning that
+            // we know its final runtime stack position, but it is
+            // not yet on the stack.
+            if !local.initialised {
+                return LocalPosition::Recursive(idx);
+            }
+
+            return LocalPosition::Known(idx);
+        }
+
+        LocalPosition::Unknown
+    }
+
+    /// Declare a local variable that occupies a stack slot and should
+    /// be accounted for, but is not directly accessible by users
+    /// (e.g. attribute sets used for `with`).
+    pub fn declare_phantom(&mut self, span: codemap::Span, initialised: bool) -> LocalIdx {
+        let idx = self.locals.len();
+        self.locals.push(Local {
+            initialised,
+            span,
+            name: LocalName::Phantom,
+            depth: self.scope_depth,
+            needs_finaliser: false,
+            must_thunk: false,
+            used: true,
+        });
+
+        LocalIdx(idx)
+    }
+
+    /// Declare an uninitialised, named local variable.
+    ///
+    /// Returns the `LocalIdx` of the new local, and optionally the
+    /// index of a previous local shadowed by this one.
+    pub fn declare_local(
+        &mut self,
+        name: String,
+        span: codemap::Span,
+    ) -> (LocalIdx, Option<LocalIdx>) {
+        let idx = LocalIdx(self.locals.len());
+        self.locals.push(Local {
+            name: LocalName::Ident(name.clone()),
+            span,
+            depth: self.scope_depth,
+            initialised: false,
+            needs_finaliser: false,
+            must_thunk: false,
+            used: false,
+        });
+
+        let mut shadowed = None;
+        match self.by_name.entry(name) {
+            hash_map::Entry::Occupied(mut entry) => {
+                let existing = entry.get_mut();
+                shadowed = Some(existing.index());
+                existing.add_idx(idx);
+            }
+            hash_map::Entry::Vacant(entry) => {
+                entry.insert(ByName::Single(idx));
+            }
+        }
+
+        (idx, shadowed)
+    }
+
+    /// Mark local as initialised after compiling its expression.
+    pub fn mark_initialised(&mut self, idx: LocalIdx) {
+        self.locals[idx.0].initialised = true;
+    }
+
+    /// Mark local as needing a finaliser.
+    pub fn mark_needs_finaliser(&mut self, idx: LocalIdx) {
+        self.locals[idx.0].needs_finaliser = true;
+    }
+
+    /// Mark local as must be wrapped in a thunk.  This happens if
+    /// the local has a reference to itself in its upvalues.
+    pub fn mark_must_thunk(&mut self, idx: LocalIdx) {
+        self.locals[idx.0].must_thunk = true;
+    }
+
+    /// Compute the runtime stack index for a given local by
+    /// accounting for uninitialised variables at scopes below this
+    /// one.
+    pub fn stack_index(&self, idx: LocalIdx) -> StackIdx {
+        let uninitialised_count = self.locals[..(idx.0)]
+            .iter()
+            .filter(|l| !l.initialised && self[idx].depth > l.depth)
+            .count();
+
+        StackIdx(idx.0 - uninitialised_count)
+    }
+
+    /// Increase the current scope depth (e.g. within a new bindings
+    /// block, or `with`-scope).
+    pub fn begin_scope(&mut self) {
+        self.scope_depth += 1;
+    }
+
+    /// Decrease the scope depth and remove all locals still tracked
+    /// for the current scope.
+    ///
+    /// Returns the count of locals that were dropped while marked as
+    /// initialised (used by the compiler to determine whether to emit
+    /// scope cleanup operations), as well as the spans of the
+    /// definitions of unused locals (used by the compiler to emit
+    /// unused binding warnings).
+    pub fn end_scope(&mut self) -> (usize, Vec<codemap::Span>) {
+        debug_assert!(self.scope_depth != 0, "can not end top scope");
+
+        let mut pops = 0;
+        let mut unused_spans = vec![];
+
+        // TL;DR - iterate from the back while things belonging to the
+        // ended scope still exist.
+        while self.locals.last().unwrap().depth == self.scope_depth {
+            if let Some(local) = self.locals.pop() {
+                // pop the local from the stack if it was actually
+                // initialised
+                if local.initialised {
+                    pops += 1;
+                }
+
+                // analyse whether the local was accessed during its
+                // lifetime, and emit a warning otherwise (unless the
+                // user explicitly chose to ignore it by prefixing the
+                // identifier with `_`)
+                if !local.used && !local.is_ignored() {
+                    unused_spans.push(local.span);
+                }
+
+                // remove the by-name index if this was a named local
+                if let LocalName::Ident(name) = local.name {
+                    if let hash_map::Entry::Occupied(mut entry) = self.by_name.entry(name) {
+                        // If no removal occured through `remove_idx`
+                        // (i.e. there was no shadowing going on),
+                        // nuke the whole entry.
+                        if !entry.get_mut().remove_idx() {
+                            entry.remove();
+                        }
+                    }
+                }
+            }
+        }
+
+        self.scope_depth -= 1;
+
+        (pops, unused_spans)
+    }
+
+    /// Access the current scope depth.
+    pub fn scope_depth(&self) -> usize {
+        self.scope_depth
+    }
+}
diff --git a/tvix/eval/src/errors.rs b/tvix/eval/src/errors.rs
new file mode 100644
index 000000000000..2fbb6496ceea
--- /dev/null
+++ b/tvix/eval/src/errors.rs
@@ -0,0 +1,1054 @@
+use std::error;
+use std::io;
+use std::path::PathBuf;
+use std::rc::Rc;
+use std::str::Utf8Error;
+use std::string::FromUtf8Error;
+use std::sync::Arc;
+use std::{fmt::Debug, fmt::Display, num::ParseIntError};
+
+use codemap::{File, Span};
+use codemap_diagnostic::{ColorConfig, Diagnostic, Emitter, Level, SpanLabel, SpanStyle};
+use smol_str::SmolStr;
+use xml::writer::Error as XmlError;
+
+use crate::spans::ToSpan;
+use crate::value::{CoercionKind, NixString};
+use crate::{SourceCode, Value};
+
+#[derive(Clone, Debug)]
+pub enum ErrorKind {
+    /// These are user-generated errors through builtins.
+    Throw(String),
+    Abort(String),
+    AssertionFailed,
+
+    DivisionByZero,
+
+    DuplicateAttrsKey {
+        key: String,
+    },
+
+    /// Attempted to specify an invalid key type (e.g. integer) in a
+    /// dynamic attribute name.
+    InvalidAttributeName(Value),
+
+    AttributeNotFound {
+        name: String,
+    },
+
+    /// Attempted to index into a list beyond its boundaries.
+    IndexOutOfBounds {
+        index: i64,
+    },
+
+    /// Attempted to call `builtins.tail` on an empty list.
+    TailEmptyList,
+
+    TypeError {
+        expected: &'static str,
+        actual: &'static str,
+    },
+
+    Incomparable {
+        lhs: &'static str,
+        rhs: &'static str,
+    },
+
+    /// Resolving a user-supplied angle brackets path literal failed in some way.
+    NixPathResolution(String),
+
+    /// Resolving a user-supplied relative or home-relative path literal failed in some way.
+    RelativePathResolution(String),
+
+    /// Dynamic keys are not allowed in some scopes.
+    DynamicKeyInScope(&'static str),
+
+    /// Unknown variable in statically known scope.
+    UnknownStaticVariable,
+
+    /// Unknown variable in dynamic scope (with, rec, ...).
+    UnknownDynamicVariable(String),
+
+    /// User is defining the same variable twice at the same depth.
+    VariableAlreadyDefined(Span),
+
+    /// Attempt to call something that is not callable.
+    NotCallable(&'static str),
+
+    /// Infinite recursion encountered while forcing thunks.
+    InfiniteRecursion {
+        first_force: Span,
+        suspended_at: Option<Span>,
+        content_span: Option<Span>,
+    },
+
+    ParseErrors(Vec<rnix::parser::ParseError>),
+
+    /// An error occured while executing some native code (e.g. a
+    /// builtin), and needs to be chained up.
+    NativeError {
+        gen_type: &'static str,
+        err: Box<Error>,
+    },
+
+    /// An error occured while executing Tvix bytecode, but needs to
+    /// be chained up.
+    BytecodeError(Box<Error>),
+
+    /// Given type can't be coerced to a string in the respective context
+    NotCoercibleToString {
+        from: &'static str,
+        kind: CoercionKind,
+    },
+
+    /// The given string doesn't represent an absolute path
+    NotAnAbsolutePath(PathBuf),
+
+    /// An error occurred when parsing an integer
+    ParseIntError(ParseIntError),
+
+    /// A negative integer was used as a value representing length.
+    NegativeLength {
+        length: i64,
+    },
+
+    // Errors specific to nested attribute sets and merges thereof.
+    /// Nested attributes can not be merged with an inherited value.
+    UnmergeableInherit {
+        name: SmolStr,
+    },
+
+    /// Nested attributes can not be merged with values that are not
+    /// literal attribute sets.
+    UnmergeableValue,
+
+    /// Parse errors occured while importing a file.
+    ImportParseError {
+        path: PathBuf,
+        file: Arc<File>,
+        errors: Vec<rnix::parser::ParseError>,
+    },
+
+    /// Compilation errors occured while importing a file.
+    ImportCompilerError {
+        path: PathBuf,
+        errors: Vec<Error>,
+    },
+
+    /// I/O errors
+    IO {
+        path: Option<PathBuf>,
+        error: Rc<io::Error>,
+    },
+
+    /// Errors converting JSON to a value
+    FromJsonError(String),
+
+    /// Nix value that can not be serialised to JSON.
+    NotSerialisableToJson(&'static str),
+
+    /// Errors converting TOML to a value
+    FromTomlError(String),
+
+    /// An unexpected argument was supplied to a function that takes formal parameters
+    UnexpectedArgument {
+        arg: NixString,
+        formals_span: Span,
+    },
+
+    /// Errors while serialising to XML.
+    Xml(Rc<XmlError>),
+
+    /// Variant for errors that bubble up to eval from other Tvix
+    /// components.
+    TvixError(Rc<dyn error::Error>),
+
+    /// Variant for code paths that are known bugs in Tvix (usually
+    /// issues with the compiler/VM interaction).
+    TvixBug {
+        msg: &'static str,
+        metadata: Option<Rc<dyn Debug>>,
+    },
+
+    /// Tvix internal warning for features triggered by users that are
+    /// not actually implemented yet, and without which eval can not
+    /// proceed.
+    NotImplemented(&'static str),
+
+    /// Internal variant which should disappear during error construction.
+    WithContext {
+        context: String,
+        underlying: Box<ErrorKind>,
+    },
+}
+
+impl error::Error for Error {
+    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+        match &self.kind {
+            ErrorKind::NativeError { err, .. } | ErrorKind::BytecodeError(err) => err.source(),
+            ErrorKind::ParseErrors(err) => err.first().map(|e| e as &dyn error::Error),
+            ErrorKind::ParseIntError(err) => Some(err),
+            ErrorKind::ImportParseError { errors, .. } => {
+                errors.first().map(|e| e as &dyn error::Error)
+            }
+            ErrorKind::ImportCompilerError { errors, .. } => {
+                errors.first().map(|e| e as &dyn error::Error)
+            }
+            ErrorKind::IO { error, .. } => Some(error.as_ref()),
+            ErrorKind::Xml(error) => Some(error.as_ref()),
+            ErrorKind::TvixError(error) => Some(error.as_ref()),
+            _ => None,
+        }
+    }
+}
+
+impl From<ParseIntError> for ErrorKind {
+    fn from(e: ParseIntError) -> Self {
+        Self::ParseIntError(e)
+    }
+}
+
+impl From<Utf8Error> for ErrorKind {
+    fn from(_: Utf8Error) -> Self {
+        Self::NotImplemented("FromUtf8Error not handled: https://b.tvl.fyi/issues/189")
+    }
+}
+
+impl From<FromUtf8Error> for ErrorKind {
+    fn from(_: FromUtf8Error) -> Self {
+        Self::NotImplemented("FromUtf8Error not handled: https://b.tvl.fyi/issues/189")
+    }
+}
+
+impl From<XmlError> for ErrorKind {
+    fn from(err: XmlError) -> Self {
+        Self::Xml(Rc::new(err))
+    }
+}
+
+impl From<io::Error> for ErrorKind {
+    fn from(e: io::Error) -> Self {
+        ErrorKind::IO {
+            path: None,
+            error: Rc::new(e),
+        }
+    }
+}
+
+impl ErrorKind {
+    /// Returns `true` if this error can be caught by `builtins.tryEval`
+    pub fn is_catchable(&self) -> bool {
+        match self {
+            Self::Throw(_) | Self::AssertionFailed | Self::NixPathResolution(_) => true,
+            Self::NativeError { err, .. } | Self::BytecodeError(err) => err.kind.is_catchable(),
+            _ => false,
+        }
+    }
+}
+
+impl From<serde_json::Error> for ErrorKind {
+    fn from(err: serde_json::Error) -> Self {
+        // Can't just put the `serde_json::Error` in the ErrorKind since it doesn't impl `Clone`
+        Self::FromJsonError(format!("error in JSON serialization: {err}"))
+    }
+}
+
+impl From<toml::de::Error> for ErrorKind {
+    fn from(err: toml::de::Error) -> Self {
+        Self::FromTomlError(format!("error in TOML serialization: {err}"))
+    }
+}
+
+#[derive(Clone, Debug)]
+pub struct Error {
+    pub kind: ErrorKind,
+    pub span: Span,
+    pub contexts: Vec<String>,
+}
+
+impl Error {
+    pub fn new(mut kind: ErrorKind, span: Span) -> Self {
+        let mut contexts = vec![];
+        while let ErrorKind::WithContext {
+            context,
+            underlying,
+        } = kind
+        {
+            kind = *underlying;
+            contexts.push(context);
+        }
+
+        Error {
+            kind,
+            span,
+            contexts,
+        }
+    }
+}
+
+impl Display for ErrorKind {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match &self {
+            ErrorKind::Throw(msg) => write!(f, "error thrown: {}", msg),
+            ErrorKind::Abort(msg) => write!(f, "evaluation aborted: {}", msg),
+            ErrorKind::AssertionFailed => write!(f, "assertion failed"),
+
+            ErrorKind::DivisionByZero => write!(f, "division by zero"),
+
+            ErrorKind::DuplicateAttrsKey { key } => {
+                write!(f, "attribute key '{}' already defined", key)
+            }
+
+            ErrorKind::InvalidAttributeName(val) => write!(
+                f,
+                "found attribute name '{}' of type '{}', but attribute names must be strings",
+                val,
+                val.type_of()
+            ),
+
+            ErrorKind::AttributeNotFound { name } => write!(
+                f,
+                "attribute with name '{}' could not be found in the set",
+                name
+            ),
+
+            ErrorKind::IndexOutOfBounds { index } => {
+                write!(f, "list index '{}' is out of bounds", index)
+            }
+
+            ErrorKind::TailEmptyList => write!(f, "'tail' called on an empty list"),
+
+            ErrorKind::TypeError { expected, actual } => write!(
+                f,
+                "expected value of type '{}', but found a '{}'",
+                expected, actual
+            ),
+
+            ErrorKind::Incomparable { lhs, rhs } => {
+                write!(f, "can not compare a {} with a {}", lhs, rhs)
+            }
+
+            ErrorKind::NixPathResolution(err) | ErrorKind::RelativePathResolution(err) => {
+                write!(f, "could not resolve path: {}", err)
+            }
+
+            ErrorKind::DynamicKeyInScope(scope) => {
+                write!(f, "dynamically evaluated keys are not allowed in {}", scope)
+            }
+
+            ErrorKind::UnknownStaticVariable => write!(f, "variable not found"),
+
+            ErrorKind::UnknownDynamicVariable(name) => write!(
+                f,
+                r#"variable '{}' could not be found
+
+Note that this occured within a `with`-expression. The problem may be related
+to a missing value in the attribute set(s) included via `with`."#,
+                name
+            ),
+
+            ErrorKind::VariableAlreadyDefined(_) => write!(f, "variable has already been defined"),
+
+            ErrorKind::NotCallable(other_type) => {
+                write!(
+                    f,
+                    "only functions and builtins can be called, but this is a '{}'",
+                    other_type
+                )
+            }
+
+            ErrorKind::InfiniteRecursion { .. } => write!(f, "infinite recursion encountered"),
+
+            // Errors themselves ignored here & handled in Self::spans instead
+            ErrorKind::ParseErrors(_) => write!(f, "failed to parse Nix code:"),
+
+            ErrorKind::NativeError { gen_type, .. } => {
+                write!(f, "while evaluating this as native code ({})", gen_type)
+            }
+
+            ErrorKind::BytecodeError(_) => write!(f, "while evaluating this Nix code"),
+
+            ErrorKind::NotCoercibleToString { kind, from } => {
+                let kindly = match kind {
+                    CoercionKind::Strong => "strongly",
+                    CoercionKind::Weak => "weakly",
+                };
+
+                let hint = if *from == "set" {
+                    ", missing a `__toString` or `outPath` attribute"
+                } else {
+                    ""
+                };
+
+                write!(f, "cannot ({kindly}) coerce {from} to a string{hint}")
+            }
+
+            ErrorKind::NotAnAbsolutePath(given) => {
+                write!(
+                    f,
+                    "string '{}' does not represent an absolute path",
+                    given.to_string_lossy()
+                )
+            }
+
+            ErrorKind::ParseIntError(err) => {
+                write!(f, "invalid integer: {}", err)
+            }
+
+            ErrorKind::NegativeLength { length } => {
+                write!(
+                    f,
+                    "cannot use a negative integer, {}, for a value representing length",
+                    length
+                )
+            }
+
+            ErrorKind::UnmergeableInherit { name } => {
+                write!(
+                    f,
+                    "cannot merge a nested attribute set into the inherited entry '{}'",
+                    name
+                )
+            }
+
+            ErrorKind::UnmergeableValue => {
+                write!(
+                    f,
+                    "nested attribute sets or keys can only be merged with literal attribute sets"
+                )
+            }
+
+            // Errors themselves ignored here & handled in Self::spans instead
+            ErrorKind::ImportParseError { path, .. } => {
+                write!(
+                    f,
+                    "parse errors occured while importing '{}'",
+                    path.to_string_lossy()
+                )
+            }
+
+            ErrorKind::ImportCompilerError { path, .. } => {
+                writeln!(
+                    f,
+                    "compiler errors occured while importing '{}'",
+                    path.to_string_lossy()
+                )
+            }
+
+            ErrorKind::IO { path, error } => {
+                write!(f, "I/O error: ")?;
+                if let Some(path) = path {
+                    write!(f, "{}: ", path.display())?;
+                }
+                write!(f, "{error}")
+            }
+
+            ErrorKind::FromJsonError(msg) => {
+                write!(f, "Error converting JSON to a Nix value: {msg}")
+            }
+
+            ErrorKind::NotSerialisableToJson(_type) => {
+                write!(f, "a {} cannot be converted to JSON", _type)
+            }
+
+            ErrorKind::FromTomlError(msg) => {
+                write!(f, "Error converting TOML to a Nix value: {msg}")
+            }
+
+            ErrorKind::UnexpectedArgument { arg, .. } => {
+                write!(
+                    f,
+                    "Unexpected argument `{}` supplied to function",
+                    arg.as_str()
+                )
+            }
+
+            ErrorKind::Xml(error) => write!(f, "failed to serialise to XML: {error}"),
+
+            ErrorKind::TvixError(inner_error) => {
+                write!(f, "{inner_error}")
+            }
+
+            ErrorKind::TvixBug { msg, metadata } => {
+                write!(f, "Tvix bug: {}", msg)?;
+
+                if let Some(metadata) = metadata {
+                    write!(f, "; metadata: {:?}", metadata)?;
+                }
+
+                Ok(())
+            }
+
+            ErrorKind::NotImplemented(feature) => {
+                write!(f, "feature not yet implemented in Tvix: {}", feature)
+            }
+
+            ErrorKind::WithContext { .. } => {
+                panic!("internal ErrorKind::WithContext variant leaked")
+            }
+        }
+    }
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.kind)
+    }
+}
+
+pub type EvalResult<T> = Result<T, Error>;
+
+/// Human-readable names for rnix syntaxes.
+fn name_for_syntax(syntax: &rnix::SyntaxKind) -> &'static str {
+    match syntax {
+        rnix::SyntaxKind::TOKEN_COMMENT => "a comment",
+        rnix::SyntaxKind::TOKEN_WHITESPACE => "whitespace",
+        rnix::SyntaxKind::TOKEN_ASSERT => "`assert`-keyword",
+        rnix::SyntaxKind::TOKEN_ELSE => "`else`-keyword",
+        rnix::SyntaxKind::TOKEN_IN => "`in`-keyword",
+        rnix::SyntaxKind::TOKEN_IF => "`if`-keyword",
+        rnix::SyntaxKind::TOKEN_INHERIT => "`inherit`-keyword",
+        rnix::SyntaxKind::TOKEN_LET => "`let`-keyword",
+        rnix::SyntaxKind::TOKEN_OR => "`or`-keyword",
+        rnix::SyntaxKind::TOKEN_REC => "`rec`-keyword",
+        rnix::SyntaxKind::TOKEN_THEN => "`then`-keyword",
+        rnix::SyntaxKind::TOKEN_WITH => "`with`-keyword",
+        rnix::SyntaxKind::TOKEN_L_BRACE => "{",
+        rnix::SyntaxKind::TOKEN_R_BRACE => "}",
+        rnix::SyntaxKind::TOKEN_L_BRACK => "[",
+        rnix::SyntaxKind::TOKEN_R_BRACK => "]",
+        rnix::SyntaxKind::TOKEN_ASSIGN => "=",
+        rnix::SyntaxKind::TOKEN_AT => "@",
+        rnix::SyntaxKind::TOKEN_COLON => ":",
+        rnix::SyntaxKind::TOKEN_COMMA => "`,`",
+        rnix::SyntaxKind::TOKEN_DOT => ".",
+        rnix::SyntaxKind::TOKEN_ELLIPSIS => "...",
+        rnix::SyntaxKind::TOKEN_QUESTION => "?",
+        rnix::SyntaxKind::TOKEN_SEMICOLON => ";",
+        rnix::SyntaxKind::TOKEN_L_PAREN => "(",
+        rnix::SyntaxKind::TOKEN_R_PAREN => ")",
+        rnix::SyntaxKind::TOKEN_CONCAT => "++",
+        rnix::SyntaxKind::TOKEN_INVERT => "!",
+        rnix::SyntaxKind::TOKEN_UPDATE => "//",
+        rnix::SyntaxKind::TOKEN_ADD => "+",
+        rnix::SyntaxKind::TOKEN_SUB => "-",
+        rnix::SyntaxKind::TOKEN_MUL => "*",
+        rnix::SyntaxKind::TOKEN_DIV => "/",
+        rnix::SyntaxKind::TOKEN_AND_AND => "&&",
+        rnix::SyntaxKind::TOKEN_EQUAL => "==",
+        rnix::SyntaxKind::TOKEN_IMPLICATION => "->",
+        rnix::SyntaxKind::TOKEN_LESS => "<",
+        rnix::SyntaxKind::TOKEN_LESS_OR_EQ => "<=",
+        rnix::SyntaxKind::TOKEN_MORE => ">",
+        rnix::SyntaxKind::TOKEN_MORE_OR_EQ => ">=",
+        rnix::SyntaxKind::TOKEN_NOT_EQUAL => "!=",
+        rnix::SyntaxKind::TOKEN_OR_OR => "||",
+        rnix::SyntaxKind::TOKEN_FLOAT => "a float",
+        rnix::SyntaxKind::TOKEN_IDENT => "an identifier",
+        rnix::SyntaxKind::TOKEN_INTEGER => "an integer",
+        rnix::SyntaxKind::TOKEN_INTERPOL_END => "}",
+        rnix::SyntaxKind::TOKEN_INTERPOL_START => "${",
+        rnix::SyntaxKind::TOKEN_PATH => "a path",
+        rnix::SyntaxKind::TOKEN_URI => "a literal URI",
+        rnix::SyntaxKind::TOKEN_STRING_CONTENT => "content of a string",
+        rnix::SyntaxKind::TOKEN_STRING_END => "\"",
+        rnix::SyntaxKind::TOKEN_STRING_START => "\"",
+
+        rnix::SyntaxKind::NODE_APPLY => "a function application",
+        rnix::SyntaxKind::NODE_ASSERT => "an assertion",
+        rnix::SyntaxKind::NODE_ATTRPATH => "an attribute path",
+        rnix::SyntaxKind::NODE_DYNAMIC => "a dynamic identifier",
+
+        rnix::SyntaxKind::NODE_IDENT => "an identifier",
+        rnix::SyntaxKind::NODE_IF_ELSE => "an `if`-expression",
+        rnix::SyntaxKind::NODE_SELECT => "a `select`-expression",
+        rnix::SyntaxKind::NODE_INHERIT => "inherited values",
+        rnix::SyntaxKind::NODE_INHERIT_FROM => "inherited values",
+        rnix::SyntaxKind::NODE_STRING => "a string",
+        rnix::SyntaxKind::NODE_INTERPOL => "an interpolation",
+        rnix::SyntaxKind::NODE_LAMBDA => "a function",
+        rnix::SyntaxKind::NODE_IDENT_PARAM => "a function parameter",
+        rnix::SyntaxKind::NODE_LEGACY_LET => "a legacy `let`-expression",
+        rnix::SyntaxKind::NODE_LET_IN => "a `let`-expression",
+        rnix::SyntaxKind::NODE_LIST => "a list",
+        rnix::SyntaxKind::NODE_BIN_OP => "a binary operator",
+        rnix::SyntaxKind::NODE_PAREN => "a parenthesised expression",
+        rnix::SyntaxKind::NODE_PATTERN => "a function argument pattern",
+        rnix::SyntaxKind::NODE_PAT_BIND => "an argument pattern binding",
+        rnix::SyntaxKind::NODE_PAT_ENTRY => "an argument pattern entry",
+        rnix::SyntaxKind::NODE_ROOT => "a Nix expression",
+        rnix::SyntaxKind::NODE_ATTR_SET => "an attribute set",
+        rnix::SyntaxKind::NODE_ATTRPATH_VALUE => "an attribute set entry",
+        rnix::SyntaxKind::NODE_UNARY_OP => "a unary operator",
+        rnix::SyntaxKind::NODE_LITERAL => "a literal value",
+        rnix::SyntaxKind::NODE_WITH => "a `with`-expression",
+        rnix::SyntaxKind::NODE_PATH => "a path",
+        rnix::SyntaxKind::NODE_HAS_ATTR => "`?`-operator",
+
+        // TODO(tazjin): unsure what these variants are, lets crash!
+        rnix::SyntaxKind::NODE_ERROR => todo!("NODE_ERROR found, tell tazjin!"),
+        rnix::SyntaxKind::TOKEN_ERROR => todo!("TOKEN_ERROR found, tell tazjin!"),
+        _ => todo!(),
+    }
+}
+
+/// Construct the string representation for a list of expected parser tokens.
+fn expected_syntax(one_of: &[rnix::SyntaxKind]) -> String {
+    match one_of.len() {
+        0 => "nothing".into(),
+        1 => format!("'{}'", name_for_syntax(&one_of[0])),
+        _ => {
+            let mut out: String = "one of: ".into();
+            let end = one_of.len() - 1;
+
+            for (idx, item) in one_of.iter().enumerate() {
+                if idx != 0 {
+                    out.push_str(", ");
+                } else if idx == end {
+                    out.push_str(", or ");
+                };
+
+                out.push_str(name_for_syntax(item));
+            }
+
+            out
+        }
+    }
+}
+
+/// Process a list of parse errors into a set of span labels, annotating parse
+/// errors.
+fn spans_for_parse_errors(file: &File, errors: &[rnix::parser::ParseError]) -> Vec<SpanLabel> {
+    // rnix has a tendency to emit some identical errors more than once, but
+    // they do not enhance the user experience necessarily, so we filter them
+    // out
+    let mut had_eof = false;
+
+    errors
+        .iter()
+        .enumerate()
+        .filter_map(|(idx, err)| {
+            let (span, label): (Span, String) = match err {
+                rnix::parser::ParseError::Unexpected(range) => (
+                    range.span_for(file),
+                    "found an unexpected syntax element here".into(),
+                ),
+
+                rnix::parser::ParseError::UnexpectedExtra(range) => (
+                    range.span_for(file),
+                    "found unexpected extra elements at the root of the expression".into(),
+                ),
+
+                rnix::parser::ParseError::UnexpectedWanted(found, range, wanted) => {
+                    let span = range.span_for(file);
+                    (
+                        span,
+                        format!(
+                            "found '{}', but expected {}",
+                            name_for_syntax(found),
+                            expected_syntax(wanted),
+                        ),
+                    )
+                }
+
+                rnix::parser::ParseError::UnexpectedEOF => {
+                    if had_eof {
+                        return None;
+                    }
+
+                    had_eof = true;
+
+                    (
+                        file.span,
+                        "code ended unexpectedly while the parser still expected more".into(),
+                    )
+                }
+
+                rnix::parser::ParseError::UnexpectedEOFWanted(wanted) => {
+                    had_eof = true;
+
+                    (
+                        file.span,
+                        format!(
+                            "code ended unexpectedly, but wanted {}",
+                            expected_syntax(wanted)
+                        ),
+                    )
+                }
+
+                rnix::parser::ParseError::DuplicatedArgs(range, name) => (
+                    range.span_for(file),
+                    format!(
+                        "the function argument pattern '{}' was bound more than once",
+                        name
+                    ),
+                ),
+
+                rnix::parser::ParseError::RecursionLimitExceeded => (
+                    file.span,
+                    "this code exceeds the parser's recursion limit, please report a Tvix bug"
+                        .to_string(),
+                ),
+
+                // TODO: can rnix even still throw this? it's semantic!
+                rnix::parser::ParseError::UnexpectedDoubleBind(range) => (
+                    range.span_for(file),
+                    "this pattern was bound more than once".into(),
+                ),
+
+                // The error enum is marked as `#[non_exhaustive]` in rnix,
+                // which disables the compiler error for missing a variant. This
+                // feature makes it possible for users to miss critical updates
+                // of enum variants for a more exciting runtime experience.
+                new => todo!("new parse error variant: {}", new),
+            };
+
+            Some(SpanLabel {
+                span,
+                label: Some(label),
+                style: if idx == 0 {
+                    SpanStyle::Primary
+                } else {
+                    SpanStyle::Secondary
+                },
+            })
+        })
+        .collect()
+}
+
+impl Error {
+    pub fn fancy_format_str(&self, source: &SourceCode) -> String {
+        let mut out = vec![];
+        Emitter::vec(&mut out, Some(&*source.codemap())).emit(&self.diagnostics(source));
+        String::from_utf8_lossy(&out).to_string()
+    }
+
+    /// Render a fancy, human-readable output of this error and print
+    /// it to stderr.
+    pub fn fancy_format_stderr(&self, source: &SourceCode) {
+        Emitter::stderr(ColorConfig::Auto, Some(&*source.codemap()))
+            .emit(&self.diagnostics(source));
+    }
+
+    /// Create the optional span label displayed as an annotation on
+    /// the underlined span of the error.
+    fn span_label(&self) -> Option<String> {
+        let label = match &self.kind {
+            ErrorKind::DuplicateAttrsKey { .. } => "in this attribute set",
+            ErrorKind::InvalidAttributeName(_) => "in this attribute set",
+            ErrorKind::NixPathResolution(_) | ErrorKind::RelativePathResolution(_) => {
+                "in this path literal"
+            }
+            ErrorKind::UnexpectedArgument { .. } => "in this function call",
+
+            // The spans for some errors don't have any more descriptive stuff
+            // in them, or we don't utilise it yet.
+            ErrorKind::Throw(_)
+            | ErrorKind::Abort(_)
+            | ErrorKind::AssertionFailed
+            | ErrorKind::AttributeNotFound { .. }
+            | ErrorKind::IndexOutOfBounds { .. }
+            | ErrorKind::TailEmptyList
+            | ErrorKind::TypeError { .. }
+            | ErrorKind::Incomparable { .. }
+            | ErrorKind::DivisionByZero
+            | ErrorKind::DynamicKeyInScope(_)
+            | ErrorKind::UnknownStaticVariable
+            | ErrorKind::UnknownDynamicVariable(_)
+            | ErrorKind::VariableAlreadyDefined(_)
+            | ErrorKind::NotCallable(_)
+            | ErrorKind::InfiniteRecursion { .. }
+            | ErrorKind::ParseErrors(_)
+            | ErrorKind::NativeError { .. }
+            | ErrorKind::BytecodeError(_)
+            | ErrorKind::NotCoercibleToString { .. }
+            | ErrorKind::NotAnAbsolutePath(_)
+            | ErrorKind::ParseIntError(_)
+            | ErrorKind::NegativeLength { .. }
+            | ErrorKind::UnmergeableInherit { .. }
+            | ErrorKind::UnmergeableValue
+            | ErrorKind::ImportParseError { .. }
+            | ErrorKind::ImportCompilerError { .. }
+            | ErrorKind::IO { .. }
+            | ErrorKind::FromJsonError(_)
+            | ErrorKind::NotSerialisableToJson(_)
+            | ErrorKind::FromTomlError(_)
+            | ErrorKind::Xml(_)
+            | ErrorKind::TvixError(_)
+            | ErrorKind::TvixBug { .. }
+            | ErrorKind::NotImplemented(_)
+            | ErrorKind::WithContext { .. } => return None,
+        };
+
+        Some(label.into())
+    }
+
+    /// Return the unique error code for this variant which can be
+    /// used to refer users to documentation.
+    fn code(&self) -> &'static str {
+        match self.kind {
+            ErrorKind::Throw(_) => "E001",
+            ErrorKind::Abort(_) => "E002",
+            ErrorKind::AssertionFailed => "E003",
+            ErrorKind::InvalidAttributeName { .. } => "E004",
+            ErrorKind::AttributeNotFound { .. } => "E005",
+            ErrorKind::TypeError { .. } => "E006",
+            ErrorKind::Incomparable { .. } => "E007",
+            ErrorKind::NixPathResolution(_) => "E008",
+            ErrorKind::DynamicKeyInScope(_) => "E009",
+            ErrorKind::UnknownStaticVariable => "E010",
+            ErrorKind::UnknownDynamicVariable(_) => "E011",
+            ErrorKind::VariableAlreadyDefined(_) => "E012",
+            ErrorKind::NotCallable(_) => "E013",
+            ErrorKind::InfiniteRecursion { .. } => "E014",
+            ErrorKind::ParseErrors(_) => "E015",
+            ErrorKind::DuplicateAttrsKey { .. } => "E016",
+            ErrorKind::NotCoercibleToString { .. } => "E018",
+            ErrorKind::IndexOutOfBounds { .. } => "E019",
+            ErrorKind::NotAnAbsolutePath(_) => "E020",
+            ErrorKind::ParseIntError(_) => "E021",
+            ErrorKind::NegativeLength { .. } => "E022",
+            ErrorKind::TailEmptyList { .. } => "E023",
+            ErrorKind::UnmergeableInherit { .. } => "E024",
+            ErrorKind::UnmergeableValue => "E025",
+            ErrorKind::ImportParseError { .. } => "E027",
+            ErrorKind::ImportCompilerError { .. } => "E028",
+            ErrorKind::IO { .. } => "E029",
+            ErrorKind::FromJsonError { .. } => "E030",
+            ErrorKind::UnexpectedArgument { .. } => "E031",
+            ErrorKind::RelativePathResolution(_) => "E032",
+            ErrorKind::DivisionByZero => "E033",
+            ErrorKind::Xml(_) => "E034",
+            ErrorKind::FromTomlError(_) => "E035",
+            ErrorKind::NotSerialisableToJson(_) => "E036",
+
+            // Special error code for errors from other Tvix
+            // components. We may want to introduce a code namespacing
+            // system to have these errors pass codes through.
+            ErrorKind::TvixError(_) => "E997",
+
+            // Special error code that is not part of the normal
+            // ordering.
+            ErrorKind::TvixBug { .. } => "E998",
+
+            // Placeholder error while Tvix is under construction.
+            ErrorKind::NotImplemented(_) => "E999",
+
+            // Chained errors should yield the code of the innermost
+            // error.
+            ErrorKind::NativeError { ref err, .. } | ErrorKind::BytecodeError(ref err) => {
+                err.code()
+            }
+
+            ErrorKind::WithContext { .. } => {
+                panic!("internal ErrorKind::WithContext variant leaked")
+            }
+        }
+    }
+
+    fn spans(&self, source: &SourceCode) -> Vec<SpanLabel> {
+        let mut spans = match &self.kind {
+            ErrorKind::ImportParseError { errors, file, .. } => {
+                spans_for_parse_errors(file, errors)
+            }
+
+            ErrorKind::ParseErrors(errors) => {
+                let file = source.get_file(self.span);
+                spans_for_parse_errors(&file, errors)
+            }
+
+            ErrorKind::UnexpectedArgument { formals_span, .. } => {
+                vec![
+                    SpanLabel {
+                        label: self.span_label(),
+                        span: self.span,
+                        style: SpanStyle::Primary,
+                    },
+                    SpanLabel {
+                        label: Some("the accepted arguments".into()),
+                        span: *formals_span,
+                        style: SpanStyle::Secondary,
+                    },
+                ]
+            }
+
+            ErrorKind::InfiniteRecursion {
+                first_force,
+                suspended_at,
+                content_span,
+            } => {
+                let mut spans = vec![];
+
+                if let Some(content_span) = content_span {
+                    spans.push(SpanLabel {
+                        label: Some("this lazily-evaluated code".into()),
+                        span: *content_span,
+                        style: SpanStyle::Secondary,
+                    })
+                }
+
+                if let Some(suspended_at) = suspended_at {
+                    spans.push(SpanLabel {
+                        label: Some("which was instantiated here".into()),
+                        span: *suspended_at,
+                        style: SpanStyle::Secondary,
+                    })
+                }
+
+                spans.push(SpanLabel {
+                    label: Some("was first requested to be evaluated here".into()),
+                    span: *first_force,
+                    style: SpanStyle::Secondary,
+                });
+
+                spans.push(SpanLabel {
+                    label: Some("but then requested again here during its own evaluation".into()),
+                    span: self.span,
+                    style: SpanStyle::Primary,
+                });
+
+                spans
+            }
+
+            // All other errors pretty much have the same shape.
+            _ => {
+                vec![SpanLabel {
+                    label: self.span_label(),
+                    span: self.span,
+                    style: SpanStyle::Primary,
+                }]
+            }
+        };
+
+        for ctx in &self.contexts {
+            spans.push(SpanLabel {
+                label: Some(format!("while {}", ctx)),
+                span: self.span,
+                style: SpanStyle::Secondary,
+            });
+        }
+
+        spans
+    }
+
+    /// Create the primary diagnostic for a given error.
+    fn diagnostic(&self, source: &SourceCode) -> Diagnostic {
+        Diagnostic {
+            level: Level::Error,
+            message: self.to_string(),
+            spans: self.spans(source),
+            code: Some(self.code().into()),
+        }
+    }
+
+    /// Return the primary diagnostic and all further associated diagnostics (if
+    /// any) of an error.
+    fn diagnostics(&self, source: &SourceCode) -> Vec<Diagnostic> {
+        match &self.kind {
+            ErrorKind::ImportCompilerError { errors, .. } => {
+                let mut out = vec![self.diagnostic(source)];
+                out.extend(errors.iter().map(|e| e.diagnostic(source)));
+                out
+            }
+
+            // When encountering either of these error kinds, we are dealing
+            // with the top of an error chain.
+            //
+            // An error chain creates a list of diagnostics which provide trace
+            // information.
+            //
+            // We don't know how deep this chain is, so we avoid recursing in
+            // this function while unrolling the chain.
+            ErrorKind::NativeError { err: next, .. } | ErrorKind::BytecodeError(next) => {
+                // Accumulated diagnostics to return.
+                let mut diagnostics: Vec<Diagnostic> = vec![];
+
+                // The next (inner) error to add to the diagnostics, after this
+                // one.
+                let mut next = *next.clone();
+
+                // Diagnostic message for *this* error.
+                let mut this_message = self.to_string();
+
+                // Primary span for *this* error.
+                let mut this_span = self.span;
+
+                // Diagnostic spans for *this* error.
+                let mut this_spans = self.spans(source);
+
+                loop {
+                    if is_new_span(
+                        this_span,
+                        diagnostics.last().and_then(|last| last.spans.last()),
+                    ) {
+                        diagnostics.push(Diagnostic {
+                            level: Level::Note,
+                            message: this_message,
+                            spans: this_spans,
+                            code: None, // only the top-level error has one
+                        });
+                    }
+
+                    this_message = next.to_string();
+                    this_span = next.span;
+                    this_spans = next.spans(source);
+
+                    match next.kind {
+                        ErrorKind::NativeError { err: inner, .. }
+                        | ErrorKind::BytecodeError(inner) => {
+                            next = *inner;
+                            continue;
+                        }
+                        _ => {
+                            diagnostics.extend(next.diagnostics(source));
+                            break;
+                        }
+                    }
+                }
+
+                diagnostics
+            }
+
+            _ => vec![self.diagnostic(source)],
+        }
+    }
+}
+
+// Check if this error is in a different span from its immediate ancestor.
+fn is_new_span(this_span: Span, parent: Option<&SpanLabel>) -> bool {
+    match parent {
+        None => true,
+        Some(parent) => parent.span != this_span,
+    }
+}
+
+// Convenience methods to add context on other types.
+pub trait AddContext {
+    /// Add context to the error-carrying type.
+    fn context<S: Into<String>>(self, ctx: S) -> Self;
+}
+
+impl AddContext for ErrorKind {
+    fn context<S: Into<String>>(self, ctx: S) -> Self {
+        ErrorKind::WithContext {
+            context: ctx.into(),
+            underlying: Box::new(self),
+        }
+    }
+}
+
+impl<T> AddContext for Result<T, ErrorKind> {
+    fn context<S: Into<String>>(self, ctx: S) -> Self {
+        self.map_err(|kind| kind.context(ctx))
+    }
+}
+
+impl<T> AddContext for Result<T, Error> {
+    fn context<S: Into<String>>(self, ctx: S) -> Self {
+        self.map_err(|err| Error {
+            kind: err.kind.context(ctx),
+            ..err
+        })
+    }
+}
diff --git a/tvix/eval/src/io.rs b/tvix/eval/src/io.rs
new file mode 100644
index 000000000000..708c36153c47
--- /dev/null
+++ b/tvix/eval/src/io.rs
@@ -0,0 +1,138 @@
+//! Interface for injecting I/O-related functionality into tvix-eval.
+//!
+//! The Nix language contains several builtins (e.g. `builtins.readDir`), as
+//! well as language feature (e.g. string-"coercion" of paths) that interact
+//! with the filesystem.
+//!
+//! The language evaluator implemented by this crate does not depend on any
+//! particular filesystem interaction model. Instead, this module provides a
+//! trait that can be implemented by tvix-eval callers to provide the
+//! functionality they desire.
+//!
+//! In theory this can be used to implement "mocked" filesystem interactions, or
+//! interaction with remote filesystems, etc.
+//!
+//! In the context of Nix builds, callers also use this interface to determine
+//! how store paths are opened and so on.
+
+use smol_str::SmolStr;
+use std::{
+    io,
+    path::{Path, PathBuf},
+};
+
+/// Types of files as represented by `builtins.readDir` in Nix.
+#[derive(Debug)]
+pub enum FileType {
+    Directory,
+    Regular,
+    Symlink,
+    Unknown,
+}
+
+/// Defines how filesystem interaction occurs inside of tvix-eval.
+pub trait EvalIO {
+    /// Verify whether the file at the specified path exists.
+    fn path_exists(&self, path: &Path) -> Result<bool, io::Error>;
+
+    /// Read the file at the specified path to a string.
+    fn read_to_string(&self, path: &Path) -> Result<String, io::Error>;
+
+    /// Read the directory at the specified path and return the names
+    /// of its entries associated with their [`FileType`].
+    fn read_dir(&self, path: &Path) -> Result<Vec<(SmolStr, FileType)>, io::Error>;
+
+    /// Import the given path. What this means depends on the
+    /// implementation, for example for a `std::io`-based
+    /// implementation this might be a no-op, while for a Tvix store
+    /// this might be a copy of the given files to the store.
+    ///
+    /// This is primarily used in the context of things like coercing
+    /// a local path to a string, or builtins like `path`.
+    fn import_path(&self, path: &Path) -> Result<PathBuf, io::Error>;
+
+    /// Returns the root of the store directory, if such a thing
+    /// exists in the evaluation context.
+    fn store_dir(&self) -> Option<String> {
+        None
+    }
+}
+
+/// Implementation of [`EvalIO`] that simply uses the equivalent
+/// standard library functions, i.e. does local file-IO.
+#[cfg(feature = "impure")]
+pub struct StdIO;
+
+#[cfg(feature = "impure")]
+impl EvalIO for StdIO {
+    fn path_exists(&self, path: &Path) -> Result<bool, io::Error> {
+        path.try_exists()
+    }
+
+    fn read_to_string(&self, path: &Path) -> Result<String, io::Error> {
+        std::fs::read_to_string(&path)
+    }
+
+    fn read_dir(&self, path: &Path) -> Result<Vec<(SmolStr, FileType)>, io::Error> {
+        let mut result = vec![];
+
+        for entry in path.read_dir()? {
+            let entry = entry?;
+            let file_type = entry.metadata()?.file_type();
+
+            let val = if file_type.is_dir() {
+                FileType::Directory
+            } else if file_type.is_file() {
+                FileType::Regular
+            } else if file_type.is_symlink() {
+                FileType::Symlink
+            } else {
+                FileType::Unknown
+            };
+
+            result.push((SmolStr::new(entry.file_name().to_string_lossy()), val));
+        }
+
+        Ok(result)
+    }
+
+    // this is a no-op for `std::io`, as the user can already refer to
+    // the path directly
+    fn import_path(&self, path: &Path) -> Result<PathBuf, io::Error> {
+        Ok(path.to_path_buf())
+    }
+}
+
+/// Dummy implementation of [`EvalIO`], can be used in contexts where
+/// IO is not available but code should "pretend" that it is.
+pub struct DummyIO;
+
+impl EvalIO for DummyIO {
+    fn path_exists(&self, _: &Path) -> Result<bool, io::Error> {
+        Err(io::Error::new(
+            io::ErrorKind::Unsupported,
+            "I/O methods are not implemented in DummyIO",
+        ))
+    }
+
+    fn read_to_string(&self, _: &Path) -> Result<String, io::Error> {
+        Err(io::Error::new(
+            io::ErrorKind::Unsupported,
+            "I/O methods are not implemented in DummyIO",
+        ))
+    }
+
+    fn read_dir(&self, _: &Path) -> Result<Vec<(SmolStr, FileType)>, io::Error> {
+        Err(io::Error::new(
+            io::ErrorKind::Unsupported,
+            "I/O methods are not implemented in DummyIO",
+        ))
+    }
+
+    fn import_path(&self, _: &Path) -> Result<PathBuf, io::Error> {
+        Err(io::Error::new(
+            io::ErrorKind::Unsupported,
+            "I/O methods are not implemented in DummyIO",
+        ))
+    }
+}
diff --git a/tvix/eval/src/lib.rs b/tvix/eval/src/lib.rs
new file mode 100644
index 000000000000..deb48b50078e
--- /dev/null
+++ b/tvix/eval/src/lib.rs
@@ -0,0 +1,346 @@
+//! `tvix-eval` implements the evaluation of the Nix programming language in
+//! Tvix.
+//!
+//! It is designed to allow users to use Nix as a versatile language for
+//! different use-cases.
+//!
+//! This module exports the high-level functions and types needed for evaluating
+//! Nix code and interacting with the language's data structures.
+//!
+//! Nix has several language features that make use of impurities (such as
+//! reading from the NIX_PATH environment variable, or interacting with files).
+//! These features are optional and the API of this crate exposes functionality
+//! for controlling how they work.
+
+pub mod builtins;
+mod chunk;
+mod compiler;
+mod errors;
+mod io;
+pub mod observer;
+mod opcode;
+mod pretty_ast;
+mod source;
+mod spans;
+mod systems;
+mod upvalues;
+mod value;
+mod vm;
+mod warnings;
+
+mod nix_search_path;
+#[cfg(test)]
+mod properties;
+#[cfg(test)]
+mod test_utils;
+#[cfg(test)]
+mod tests;
+
+use std::path::PathBuf;
+use std::rc::Rc;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use crate::compiler::GlobalsMap;
+use crate::observer::{CompilerObserver, RuntimeObserver};
+use crate::value::Lambda;
+use crate::vm::run_lambda;
+
+// Re-export the public interface used by other crates.
+pub use crate::compiler::{compile, prepare_globals, CompilationOutput};
+pub use crate::errors::{AddContext, Error, ErrorKind, EvalResult};
+pub use crate::io::{DummyIO, EvalIO, FileType};
+pub use crate::pretty_ast::pretty_print_expr;
+pub use crate::source::SourceCode;
+pub use crate::vm::generators;
+pub use crate::warnings::{EvalWarning, WarningKind};
+pub use builtin_macros;
+
+pub use crate::value::{Builtin, CoercionKind, NixAttrs, NixList, NixString, Value};
+
+#[cfg(feature = "impure")]
+pub use crate::io::StdIO;
+
+/// An `Evaluation` represents how a piece of Nix code is evaluated. It can be
+/// instantiated and configured directly, or it can be accessed through the
+/// various simplified helper methods available below.
+///
+/// Public fields are intended to be set by the caller. Setting all
+/// fields is optional.
+pub struct Evaluation<'code, 'co, 'ro> {
+    /// The Nix source code to be evaluated.
+    code: &'code str,
+
+    /// Optional location of the source code (i.e. path to the file it was read
+    /// from). Used for error reporting, and for resolving relative paths in
+    /// impure functions.
+    location: Option<PathBuf>,
+
+    /// Source code map used for error reporting.
+    source_map: SourceCode,
+
+    /// Top-level file reference for this code inside the source map.
+    file: Arc<codemap::File>,
+
+    /// Set of all builtins that should be available during the
+    /// evaluation.
+    ///
+    /// This defaults to all pure builtins. Users might want to add
+    /// the set of impure builtins, or other custom builtins.
+    pub builtins: Vec<(&'static str, Value)>,
+
+    /// Set of builtins that are implemented in Nix itself and should
+    /// be compiled and inserted in the builtins set.
+    pub src_builtins: Vec<(&'static str, &'static str)>,
+
+    /// Implementation of file-IO to use during evaluation, e.g. for
+    /// impure builtins.
+    ///
+    /// Defaults to [`DummyIO`] if not set explicitly.
+    pub io_handle: Box<dyn EvalIO>,
+
+    /// Determines whether the `import` builtin should be made
+    /// available. Note that this depends on the `io_handle` being
+    /// able to read the files specified as arguments to `import`.
+    pub enable_import: bool,
+
+    /// Determines whether the returned value should be strictly
+    /// evaluated, that is whether its list and attribute set elements
+    /// should be forced recursively.
+    pub strict: bool,
+
+    /// (optional) Nix search path, e.g. the value of `NIX_PATH` used
+    /// for resolving items on the search path (such as `<nixpkgs>`).
+    pub nix_path: Option<String>,
+
+    /// (optional) compiler observer for reporting on compilation
+    /// details, like the emitted bytecode.
+    pub compiler_observer: Option<&'co mut dyn CompilerObserver>,
+
+    /// (optional) runtime observer, for reporting on execution steps
+    /// of Nix code.
+    pub runtime_observer: Option<&'ro mut dyn RuntimeObserver>,
+}
+
+/// Result of evaluating a piece of Nix code. If evaluation succeeded, a value
+/// will be present (and potentially some warnings!). If evaluation failed,
+/// errors will be present.
+#[derive(Debug, Default)]
+pub struct EvaluationResult {
+    /// Nix value that the code evaluated to.
+    pub value: Option<Value>,
+
+    /// Errors that occured during evaluation (if any).
+    pub errors: Vec<Error>,
+
+    /// Warnings that occured during evaluation. Warnings are not critical, but
+    /// should be addressed either to modernise code or improve performance.
+    pub warnings: Vec<EvalWarning>,
+
+    /// AST node that was parsed from the code (on success only).
+    pub expr: Option<rnix::ast::Expr>,
+}
+
+impl<'code, 'co, 'ro> Evaluation<'code, 'co, 'ro> {
+    /// Initialise an `Evaluation` for the given Nix source code snippet, and
+    /// an optional code location.
+    pub fn new(code: &'code str, location: Option<PathBuf>) -> Self {
+        let source_map = SourceCode::new();
+
+        let location_str = location
+            .as_ref()
+            .map(|p| p.to_string_lossy().to_string())
+            .unwrap_or_else(|| "[code]".into());
+
+        let file = source_map.add_file(location_str, code.into());
+
+        let mut builtins = builtins::pure_builtins();
+        builtins.extend(builtins::placeholders()); // these are temporary
+
+        Evaluation {
+            code,
+            location,
+            source_map,
+            file,
+            builtins,
+            src_builtins: vec![],
+            io_handle: Box::new(DummyIO {}),
+            enable_import: false,
+            strict: false,
+            nix_path: None,
+            compiler_observer: None,
+            runtime_observer: None,
+        }
+    }
+
+    #[cfg(feature = "impure")]
+    /// Initialise an `Evaluation` for the given snippet, with all
+    /// impure features turned on by default.
+    pub fn new_impure(code: &'code str, location: Option<PathBuf>) -> Self {
+        let mut eval = Self::new(code, location);
+        eval.enable_import = true;
+        eval.builtins.extend(builtins::impure_builtins());
+        eval.io_handle = Box::new(StdIO);
+        eval
+    }
+
+    /// Clone the reference to the contained source code map. This is used after
+    /// an evaluation for pretty error printing.
+    pub fn source_map(&self) -> SourceCode {
+        self.source_map.clone()
+    }
+
+    /// Only compile the provided source code. This does not *run* the
+    /// code, it only provides analysis (errors and warnings) of the
+    /// compiler.
+    pub fn compile_only(mut self) -> EvaluationResult {
+        let mut result = EvaluationResult::default();
+        let source = self.source_map();
+
+        let mut noop_observer = observer::NoOpObserver::default();
+        let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer);
+
+        parse_compile_internal(
+            &mut result,
+            self.code,
+            self.file.clone(),
+            self.location,
+            source,
+            self.builtins,
+            self.src_builtins,
+            self.enable_import,
+            compiler_observer,
+        );
+
+        result
+    }
+
+    /// Evaluate the provided source code.
+    pub fn evaluate(mut self) -> EvaluationResult {
+        let mut result = EvaluationResult::default();
+        let source = self.source_map();
+
+        let mut noop_observer = observer::NoOpObserver::default();
+        let compiler_observer = self.compiler_observer.take().unwrap_or(&mut noop_observer);
+
+        // Insert a storeDir builtin *iff* a store directory is present.
+        if let Some(store_dir) = self.io_handle.store_dir() {
+            self.builtins.push(("storeDir", store_dir.into()));
+        }
+
+        let (lambda, globals) = match parse_compile_internal(
+            &mut result,
+            self.code,
+            self.file.clone(),
+            self.location,
+            source,
+            self.builtins,
+            self.src_builtins,
+            self.enable_import,
+            compiler_observer,
+        ) {
+            None => return result,
+            Some(cr) => cr,
+        };
+
+        // If bytecode was returned, there were no errors and the
+        // code is safe to execute.
+
+        let nix_path = self
+            .nix_path
+            .as_ref()
+            .and_then(|s| match nix_search_path::NixSearchPath::from_str(s) {
+                Ok(path) => Some(path),
+                Err(err) => {
+                    result.warnings.push(EvalWarning {
+                        kind: WarningKind::InvalidNixPath(err.to_string()),
+                        span: self.file.span,
+                    });
+                    None
+                }
+            })
+            .unwrap_or_default();
+
+        let runtime_observer = self.runtime_observer.take().unwrap_or(&mut noop_observer);
+
+        let vm_result = run_lambda(
+            nix_path,
+            self.io_handle,
+            runtime_observer,
+            globals,
+            lambda,
+            self.strict,
+        );
+
+        match vm_result {
+            Ok(mut runtime_result) => {
+                result.warnings.append(&mut runtime_result.warnings);
+                result.value = Some(runtime_result.value);
+            }
+            Err(err) => {
+                result.errors.push(err);
+            }
+        }
+
+        result
+    }
+}
+
+/// Internal helper function for common parsing & compilation logic
+/// between the public functions.
+fn parse_compile_internal(
+    result: &mut EvaluationResult,
+    code: &str,
+    file: Arc<codemap::File>,
+    location: Option<PathBuf>,
+    source: SourceCode,
+    builtins: Vec<(&'static str, Value)>,
+    src_builtins: Vec<(&'static str, &'static str)>,
+    enable_import: bool,
+    compiler_observer: &mut dyn CompilerObserver,
+) -> Option<(Rc<Lambda>, Rc<GlobalsMap>)> {
+    let parsed = rnix::ast::Root::parse(code);
+    let parse_errors = parsed.errors();
+
+    if !parse_errors.is_empty() {
+        result.errors.push(Error::new(
+            ErrorKind::ParseErrors(parse_errors.to_vec()),
+            file.span,
+        ));
+        return None;
+    }
+
+    // At this point we know that the code is free of parse errors and
+    // we can continue to compile it. The expression is persisted in
+    // the result, in case the caller needs it for something.
+    result.expr = parsed.tree().expr();
+
+    let builtins = crate::compiler::prepare_globals(builtins, src_builtins, source, enable_import);
+
+    let compiler_result = match compiler::compile(
+        result.expr.as_ref().unwrap(),
+        location,
+        file,
+        builtins,
+        compiler_observer,
+    ) {
+        Ok(result) => result,
+        Err(err) => {
+            result.errors.push(err);
+            return None;
+        }
+    };
+
+    result.warnings = compiler_result.warnings;
+    result.errors.extend(compiler_result.errors);
+
+    // Short-circuit if errors exist at this point (do not pass broken
+    // bytecode to the runtime).
+    if !result.errors.is_empty() {
+        return None;
+    }
+
+    // Return the lambda (for execution) and the globals map (to
+    // ensure the invariant that the globals outlive the runtime).
+    Some((compiler_result.lambda, compiler_result.globals))
+}
diff --git a/tvix/eval/src/nix_search_path.rs b/tvix/eval/src/nix_search_path.rs
new file mode 100644
index 000000000000..79c19752f6c1
--- /dev/null
+++ b/tvix/eval/src/nix_search_path.rs
@@ -0,0 +1,243 @@
+use path_clean::PathClean;
+use std::convert::Infallible;
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+
+use crate::errors::ErrorKind;
+use crate::EvalIO;
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+enum NixSearchPathEntry {
+    /// Resolve subdirectories of this path within `<...>` brackets. This
+    /// corresponds to bare paths within the `NIX_PATH` environment variable
+    ///
+    /// For example, with `NixSearchPathEntry::Path("/example")` and the following
+    /// directory structure:
+    ///
+    /// ```notrust
+    /// example
+    /// └── subdir
+    ///     └── grandchild
+    /// ```
+    ///
+    /// A Nix path literal `<subdir>` would resolve to `/example/subdir`, and a
+    /// Nix path literal `<subdir/grandchild>` would resolve to
+    /// `/example/subdir/grandchild`
+    Path(PathBuf),
+
+    /// Resolve paths starting with `prefix` as subdirectories of `path`. This
+    /// corresponds to `prefix=path` within the `NIX_PATH` environment variable.
+    ///
+    /// For example, with `NixSearchPathEntry::Prefix { prefix: "prefix", path:
+    /// "/example" }` and the following directory structure:
+    ///
+    /// ```notrust
+    /// example
+    /// └── subdir
+    ///     └── grandchild
+    /// ```
+    ///
+    /// A Nix path literal `<prefix/subdir>` would resolve to `/example/subdir`,
+    /// and a Nix path literal `<prefix/subdir/grandchild>` would resolve to
+    /// `/example/subdir/grandchild`
+    Prefix { prefix: PathBuf, path: PathBuf },
+}
+
+fn canonicalise(path: PathBuf) -> Result<PathBuf, ErrorKind> {
+    let absolute = if path.is_absolute() {
+        path
+    } else {
+        // TODO(tazjin): probably panics in wasm?
+        std::env::current_dir()
+            .map_err(|e| ErrorKind::IO {
+                path: Some(path.clone()),
+                error: e.into(),
+            })?
+            .join(path)
+    }
+    .clean();
+
+    Ok(absolute)
+}
+
+impl NixSearchPathEntry {
+    /// Determine whether this path entry matches the given lookup path.
+    ///
+    /// For bare paths, an entry is considered to match if a matching
+    /// file exists under it.
+    ///
+    /// For prefixed path, an entry matches if the prefix does.
+    // TODO(tazjin): verify these rules in the C++ impl, seems fishy.
+    fn resolve(
+        &self,
+        io: &mut dyn EvalIO,
+        lookup_path: &Path,
+    ) -> Result<Option<PathBuf>, ErrorKind> {
+        let path = match self {
+            NixSearchPathEntry::Path(parent) => canonicalise(parent.join(lookup_path))?,
+
+            NixSearchPathEntry::Prefix { prefix, path } => {
+                if let Ok(child_path) = lookup_path.strip_prefix(prefix) {
+                    canonicalise(path.join(child_path))?
+                } else {
+                    return Ok(None);
+                }
+            }
+        };
+
+        if io.path_exists(&path).map_err(|e| ErrorKind::IO {
+            path: Some(path.clone()),
+            error: e.into(),
+        })? {
+            Ok(Some(path))
+        } else {
+            Ok(None)
+        }
+    }
+}
+
+impl FromStr for NixSearchPathEntry {
+    type Err = Infallible;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.split_once('=') {
+            Some((prefix, path)) => Ok(Self::Prefix {
+                prefix: prefix.into(),
+                path: path.into(),
+            }),
+            None => Ok(Self::Path(s.into())),
+        }
+    }
+}
+
+/// Struct implementing the format and path resolution rules of the `NIX_PATH`
+/// environment variable.
+///
+/// This struct can be constructed by parsing a string using the [`FromStr`]
+/// impl, or via [`str::parse`]. Nix `<...>` paths can then be resolved using
+/// [`NixSearchPath::resolve`].
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
+pub struct NixSearchPath {
+    entries: Vec<NixSearchPathEntry>,
+}
+
+impl NixSearchPath {
+    /// Attempt to resolve the given `path` within this [`NixSearchPath`] using the
+    /// path resolution rules for `<...>`-style paths
+    pub fn resolve<P>(&self, io: &mut dyn EvalIO, path: P) -> Result<PathBuf, ErrorKind>
+    where
+        P: AsRef<Path>,
+    {
+        let path = path.as_ref();
+        for entry in &self.entries {
+            if let Some(p) = entry.resolve(io, path)? {
+                return Ok(p);
+            }
+        }
+        Err(ErrorKind::NixPathResolution(format!(
+            "path '{}' was not found in the Nix search path",
+            path.display()
+        )))
+    }
+}
+
+impl FromStr for NixSearchPath {
+    type Err = Infallible;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let entries = s
+            .split(':')
+            .map(|s| s.parse())
+            .collect::<Result<Vec<_>, _>>()?;
+        Ok(NixSearchPath { entries })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    mod parse {
+        use super::*;
+
+        #[test]
+        fn bare_paths() {
+            assert_eq!(
+                NixSearchPath::from_str("/foo/bar:/baz").unwrap(),
+                NixSearchPath {
+                    entries: vec![
+                        NixSearchPathEntry::Path("/foo/bar".into()),
+                        NixSearchPathEntry::Path("/baz".into())
+                    ],
+                }
+            );
+        }
+
+        #[test]
+        fn mixed_prefix_and_paths() {
+            assert_eq!(
+                NixSearchPath::from_str("nixpkgs=/my/nixpkgs:/etc/nixos").unwrap(),
+                NixSearchPath {
+                    entries: vec![
+                        NixSearchPathEntry::Prefix {
+                            prefix: "nixpkgs".into(),
+                            path: "/my/nixpkgs".into()
+                        },
+                        NixSearchPathEntry::Path("/etc/nixos".into())
+                    ],
+                }
+            );
+        }
+    }
+
+    mod resolve {
+        use crate::StdIO;
+        use path_clean::PathClean;
+        use std::env::current_dir;
+
+        use super::*;
+
+        #[test]
+        fn simple_dir() {
+            let nix_search_path = NixSearchPath::from_str("./.").unwrap();
+            let mut io = StdIO {};
+            let res = nix_search_path.resolve(&mut io, "src").unwrap();
+            assert_eq!(res, current_dir().unwrap().join("src").clean());
+        }
+
+        #[test]
+        fn failed_resolution() {
+            let nix_search_path = NixSearchPath::from_str("./.").unwrap();
+            let mut io = StdIO {};
+            let err = nix_search_path.resolve(&mut io, "nope").unwrap_err();
+            assert!(
+                matches!(err, ErrorKind::NixPathResolution(..)),
+                "err = {err:?}"
+            );
+        }
+
+        #[test]
+        fn second_in_path() {
+            let nix_search_path = NixSearchPath::from_str("./.:/").unwrap();
+            let mut io = StdIO {};
+            let res = nix_search_path.resolve(&mut io, "etc").unwrap();
+            assert_eq!(res, Path::new("/etc"));
+        }
+
+        #[test]
+        fn prefix() {
+            let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
+            let mut io = StdIO {};
+            let res = nix_search_path.resolve(&mut io, "tvix/src").unwrap();
+            assert_eq!(res, current_dir().unwrap().join("src").clean());
+        }
+
+        #[test]
+        fn matching_prefix() {
+            let nix_search_path = NixSearchPath::from_str("/:tvix=.").unwrap();
+            let mut io = StdIO {};
+            let res = nix_search_path.resolve(&mut io, "tvix").unwrap();
+            assert_eq!(res, current_dir().unwrap().clean());
+        }
+    }
+}
diff --git a/tvix/eval/src/observer.rs b/tvix/eval/src/observer.rs
new file mode 100644
index 000000000000..cdc6ca16ff97
--- /dev/null
+++ b/tvix/eval/src/observer.rs
@@ -0,0 +1,287 @@
+//! Implements traits for things that wish to observe internal state
+//! changes of tvix-eval.
+//!
+//! This can be used to gain insights from compilation, to trace the
+//! runtime, and so on.
+//!
+//! All methods are optional, that is, observers can implement only
+/// what they are interested in observing.
+use std::io::Write;
+use std::rc::Rc;
+use tabwriter::TabWriter;
+
+use crate::chunk::Chunk;
+use crate::generators::VMRequest;
+use crate::opcode::{CodeIdx, OpCode};
+use crate::value::Lambda;
+use crate::SourceCode;
+use crate::Value;
+
+/// Implemented by types that wish to observe internal happenings of
+/// the Tvix compiler.
+pub trait CompilerObserver {
+    /// Called when the compiler finishes compilation of the top-level
+    /// of an expression (usually the root Nix expression of a file).
+    fn observe_compiled_toplevel(&mut self, _: &Rc<Lambda>) {}
+
+    /// Called when the compiler finishes compilation of a
+    /// user-defined function.
+    ///
+    /// Note that in Nix there are only single argument functions, so
+    /// in an expression like `a: b: c: ...` this method will be
+    /// called three times.
+    fn observe_compiled_lambda(&mut self, _: &Rc<Lambda>) {}
+
+    /// Called when the compiler finishes compilation of a thunk.
+    fn observe_compiled_thunk(&mut self, _: &Rc<Lambda>) {}
+}
+
+/// Implemented by types that wish to observe internal happenings of
+/// the Tvix virtual machine at runtime.
+pub trait RuntimeObserver {
+    /// Called when the runtime enters a new call frame.
+    fn observe_enter_call_frame(&mut self, _arg_count: usize, _: &Rc<Lambda>, _call_depth: usize) {}
+
+    /// Called when the runtime exits a call frame.
+    fn observe_exit_call_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
+
+    /// Called when the runtime suspends a call frame.
+    fn observe_suspend_call_frame(&mut self, _frame_at: usize, _stack: &[Value]) {}
+
+    /// Called when the runtime enters a generator frame.
+    fn observe_enter_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
+
+    /// Called when the runtime exits a generator frame.
+    fn observe_exit_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
+
+    /// Called when the runtime suspends a generator frame.
+    fn observe_suspend_generator(&mut self, _frame_at: usize, _name: &str, _stack: &[Value]) {}
+
+    /// Called when a generator requests an action from the VM.
+    fn observe_generator_request(&mut self, _name: &str, _msg: &VMRequest) {}
+
+    /// Called when the runtime replaces the current call frame for a
+    /// tail call.
+    fn observe_tail_call(&mut self, _frame_at: usize, _: &Rc<Lambda>) {}
+
+    /// Called when the runtime enters a builtin.
+    fn observe_enter_builtin(&mut self, _name: &'static str) {}
+
+    /// Called when the runtime exits a builtin.
+    fn observe_exit_builtin(&mut self, _name: &'static str, _stack: &[Value]) {}
+
+    /// Called when the runtime *begins* executing an instruction. The
+    /// provided stack is the state at the beginning of the operation.
+    fn observe_execute_op(&mut self, _ip: CodeIdx, _: &OpCode, _: &[Value]) {}
+}
+
+#[derive(Default)]
+pub struct NoOpObserver {}
+
+impl CompilerObserver for NoOpObserver {}
+impl RuntimeObserver for NoOpObserver {}
+
+/// An observer that prints disassembled chunk information to its
+/// internal writer whenwever the compiler emits a toplevel function,
+/// closure or thunk.
+pub struct DisassemblingObserver<W: Write> {
+    source: SourceCode,
+    writer: TabWriter<W>,
+}
+
+impl<W: Write> DisassemblingObserver<W> {
+    pub fn new(source: SourceCode, writer: W) -> Self {
+        Self {
+            source,
+            writer: TabWriter::new(writer),
+        }
+    }
+
+    fn lambda_header(&mut self, kind: &str, lambda: &Rc<Lambda>) {
+        let _ = writeln!(
+            &mut self.writer,
+            "=== compiled {} @ {:p} ({} ops) ===",
+            kind,
+            *lambda,
+            lambda.chunk.code.len()
+        );
+    }
+
+    fn disassemble_chunk(&mut self, chunk: &Chunk) {
+        // calculate width of the widest address in the chunk
+        let width = format!("{:#x}", chunk.code.len() - 1).len();
+
+        for (idx, _) in chunk.code.iter().enumerate() {
+            let _ = chunk.disassemble_op(&mut self.writer, &self.source, width, CodeIdx(idx));
+        }
+    }
+}
+
+impl<W: Write> CompilerObserver for DisassemblingObserver<W> {
+    fn observe_compiled_toplevel(&mut self, lambda: &Rc<Lambda>) {
+        self.lambda_header("toplevel", lambda);
+        self.disassemble_chunk(&lambda.chunk);
+        let _ = self.writer.flush();
+    }
+
+    fn observe_compiled_lambda(&mut self, lambda: &Rc<Lambda>) {
+        self.lambda_header("lambda", lambda);
+        self.disassemble_chunk(&lambda.chunk);
+        let _ = self.writer.flush();
+    }
+
+    fn observe_compiled_thunk(&mut self, lambda: &Rc<Lambda>) {
+        self.lambda_header("thunk", lambda);
+        self.disassemble_chunk(&lambda.chunk);
+        let _ = self.writer.flush();
+    }
+}
+
+/// An observer that collects a textual representation of an entire
+/// runtime execution.
+pub struct TracingObserver<W: Write> {
+    writer: TabWriter<W>,
+}
+
+impl<W: Write> TracingObserver<W> {
+    pub fn new(writer: W) -> Self {
+        Self {
+            writer: TabWriter::new(writer),
+        }
+    }
+
+    fn write_value(&mut self, val: &Value) {
+        let _ = match val {
+            // Potentially large types which we only want to print
+            // the type of (and avoid recursing).
+            Value::List(l) => write!(&mut self.writer, "list[{}] ", l.len()),
+            Value::Attrs(a) => write!(&mut self.writer, "attrs[{}] ", a.len()),
+            Value::Thunk(t) if t.is_evaluated() => Ok(self.write_value(&t.value())),
+
+            // For other value types, defer to the standard value printer.
+            _ => write!(&mut self.writer, "{} ", val),
+        };
+    }
+
+    fn write_stack(&mut self, stack: &[Value]) {
+        let _ = write!(&mut self.writer, "[ ");
+
+        // Print out a maximum of 6 values from the top of the stack,
+        // before abbreviating it to `...`.
+        for (i, val) in stack.iter().rev().enumerate() {
+            if i == 6 {
+                let _ = write!(&mut self.writer, "...");
+                break;
+            }
+
+            self.write_value(&val);
+        }
+
+        let _ = writeln!(&mut self.writer, "]");
+    }
+}
+
+impl<W: Write> RuntimeObserver for TracingObserver<W> {
+    fn observe_enter_call_frame(
+        &mut self,
+        arg_count: usize,
+        lambda: &Rc<Lambda>,
+        call_depth: usize,
+    ) {
+        let _ = write!(&mut self.writer, "=== entering ");
+
+        let _ = if arg_count == 0 {
+            write!(&mut self.writer, "thunk ")
+        } else {
+            write!(&mut self.writer, "closure ")
+        };
+
+        if let Some(name) = &lambda.name {
+            let _ = write!(&mut self.writer, "'{}' ", name);
+        }
+
+        let _ = writeln!(
+            &mut self.writer,
+            "in frame[{}] @ {:p} ===",
+            call_depth, *lambda
+        );
+    }
+
+    /// Called when the runtime exits a call frame.
+    fn observe_exit_call_frame(&mut self, frame_at: usize, stack: &[Value]) {
+        let _ = write!(&mut self.writer, "=== exiting frame {} ===\t ", frame_at);
+        self.write_stack(stack);
+    }
+
+    fn observe_suspend_call_frame(&mut self, frame_at: usize, stack: &[Value]) {
+        let _ = write!(&mut self.writer, "=== suspending frame {} ===\t", frame_at);
+
+        self.write_stack(stack);
+    }
+
+    fn observe_enter_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
+        let _ = write!(
+            &mut self.writer,
+            "=== entering generator frame '{}' [{}] ===\t",
+            name, frame_at,
+        );
+
+        self.write_stack(stack);
+    }
+
+    fn observe_exit_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
+        let _ = write!(
+            &mut self.writer,
+            "=== exiting generator '{}' [{}] ===\t",
+            name, frame_at
+        );
+
+        self.write_stack(stack);
+    }
+
+    fn observe_suspend_generator(&mut self, frame_at: usize, name: &str, stack: &[Value]) {
+        let _ = write!(
+            &mut self.writer,
+            "=== suspending generator '{}' [{}] ===\t",
+            name, frame_at
+        );
+
+        self.write_stack(stack);
+    }
+
+    fn observe_generator_request(&mut self, name: &str, msg: &VMRequest) {
+        let _ = writeln!(
+            &mut self.writer,
+            "=== generator '{}' requested {} ===",
+            name, msg
+        );
+    }
+
+    fn observe_enter_builtin(&mut self, name: &'static str) {
+        let _ = writeln!(&mut self.writer, "=== entering builtin {} ===", name);
+    }
+
+    fn observe_exit_builtin(&mut self, name: &'static str, stack: &[Value]) {
+        let _ = write!(&mut self.writer, "=== exiting builtin {} ===\t", name);
+        self.write_stack(stack);
+    }
+
+    fn observe_tail_call(&mut self, frame_at: usize, lambda: &Rc<Lambda>) {
+        let _ = writeln!(
+            &mut self.writer,
+            "=== tail-calling {:p} in frame[{}] ===",
+            *lambda, frame_at
+        );
+    }
+
+    fn observe_execute_op(&mut self, ip: CodeIdx, op: &OpCode, stack: &[Value]) {
+        let _ = write!(&mut self.writer, "{:04} {:?}\t", ip.0, op);
+        self.write_stack(stack);
+    }
+}
+
+impl<W: Write> Drop for TracingObserver<W> {
+    fn drop(&mut self) {
+        let _ = self.writer.flush();
+    }
+}
diff --git a/tvix/eval/src/opcode.rs b/tvix/eval/src/opcode.rs
new file mode 100644
index 000000000000..130e242668d6
--- /dev/null
+++ b/tvix/eval/src/opcode.rs
@@ -0,0 +1,186 @@
+//! This module implements the instruction set running on the abstract
+//! machine implemented by tvix.
+
+use std::ops::{AddAssign, Sub};
+
+/// Index of a constant in the current code chunk.
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct ConstantIdx(pub usize);
+
+/// Index of an instruction in the current code chunk.
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug)]
+pub struct CodeIdx(pub usize);
+
+impl AddAssign<usize> for CodeIdx {
+    fn add_assign(&mut self, rhs: usize) {
+        *self = CodeIdx(self.0 + rhs)
+    }
+}
+
+impl Sub<usize> for CodeIdx {
+    type Output = Self;
+
+    fn sub(self, rhs: usize) -> Self::Output {
+        CodeIdx(self.0 - rhs)
+    }
+}
+
+/// Index of a value in the runtime stack.  This is an offset
+/// *relative to* the VM value stack_base of the CallFrame
+/// containing the opcode which contains this StackIdx.
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
+pub struct StackIdx(pub usize);
+
+/// Index of an upvalue within a closure's bound-variable upvalue
+/// list.  This is an absolute index into the Upvalues of the
+/// CallFrame containing the opcode which contains this UpvalueIdx.
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct UpvalueIdx(pub usize);
+
+/// Offset by which an instruction pointer should change in a jump.
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct JumpOffset(pub usize);
+
+/// Provided count for an instruction (could represent e.g. a number
+/// of elements).
+#[repr(transparent)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct Count(pub usize);
+
+/// All variants of this enum carry a bounded amount of data to
+/// ensure that no heap allocations are needed for an Opcode.
+#[warn(variant_size_differences)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum OpCode {
+    /// Push a constant onto the stack.
+    OpConstant(ConstantIdx),
+
+    /// Discard a value from the stack.
+    OpPop,
+
+    // Unary operators
+    OpInvert,
+    OpNegate,
+
+    // Arithmetic binary operators
+    OpAdd,
+    OpSub,
+    OpMul,
+    OpDiv,
+
+    // Comparison operators
+    OpEqual,
+    OpLess,
+    OpLessOrEq,
+    OpMore,
+    OpMoreOrEq,
+
+    // Logical operators & generic jumps
+    OpJump(JumpOffset),
+    OpJumpIfTrue(JumpOffset),
+    OpJumpIfFalse(JumpOffset),
+    OpJumpIfNotFound(JumpOffset),
+
+    // Attribute sets
+    /// Construct an attribute set from the given number of key-value pairs on the top of the stack
+    ///
+    /// Note that this takes the count of *pairs*, not the number of *stack values* - the actual
+    /// number of values popped off the stack will be twice the argument to this op
+    OpAttrs(Count),
+    OpAttrsUpdate,
+    OpAttrsSelect,
+    OpAttrsTrySelect,
+    OpHasAttr,
+
+    /// Throw an error if the attribute set at the top of the stack has any attributes
+    /// other than those listed in the formals of the current lambda
+    ///
+    /// Panics if the current frame is not a lambda with formals
+    OpValidateClosedFormals,
+
+    // `with`-handling
+    OpPushWith(StackIdx),
+    OpPopWith,
+    OpResolveWith,
+
+    // Lists
+    OpList(Count),
+    OpConcat,
+
+    // Strings
+    OpInterpolate(Count),
+    /// Force the Value on the stack and coerce it to a string, always using
+    /// `CoercionKind::Weak`.
+    OpCoerceToString,
+
+    // Paths
+    /// Attempt to resolve the Value on the stack using the configured [`NixSearchPath`][]
+    ///
+    /// [`NixSearchPath`]: crate::nix_search_path::NixSearchPath
+    OpFindFile,
+
+    /// Attempt to resolve a path literal relative to the home dir
+    OpResolveHomePath,
+
+    // Type assertion operators
+    OpAssertBool,
+
+    /// Access local identifiers with statically known positions.
+    OpGetLocal(StackIdx),
+
+    /// Close scopes while leaving their expression value around.
+    OpCloseScope(Count), // number of locals to pop
+
+    /// Return an error indicating that an `assert` failed
+    OpAssertFail,
+
+    // Lambdas & closures
+    OpCall,
+    OpGetUpvalue(UpvalueIdx),
+    /// A Closure which has upvalues but no self-references
+    OpClosure(ConstantIdx),
+    /// A Closure which has self-references (direct or via upvalues)
+    OpThunkClosure(ConstantIdx),
+    /// A suspended thunk, used to ensure laziness
+    OpThunkSuspended(ConstantIdx),
+    OpForce,
+
+    /// Finalise initialisation of the upvalues of the value in the given stack
+    /// index (which must be a Value::Thunk) after the scope is fully bound.
+    OpFinalise(StackIdx),
+
+    /// Final instruction emitted in a chunk. Does not have an
+    /// inherent effect, but can simplify VM logic as a marker in some
+    /// cases.
+    ///
+    /// Can be thought of as "returning" the value to the parent
+    /// frame, hence the name.
+    OpReturn,
+
+    // [`OpClosure`], [`OpThunkSuspended`], and [`OpThunkClosure`] have a
+    // variable number of arguments to the instruction, which is
+    // represented here by making their data part of the opcodes.
+    // Each of these two opcodes has a `ConstantIdx`, which must
+    // reference a `Value::Blueprint(Lambda)`.  The `upvalue_count`
+    // field in that `Lambda` indicates the number of arguments it
+    // takes, and the opcode must be followed by exactly this number
+    // of `Data*` opcodes.  The VM skips over these by advancing the
+    // instruction pointer.
+    //
+    // It is illegal for a `Data*` opcode to appear anywhere else.
+    /// Populate a static upvalue by copying from the stack immediately.
+    DataStackIdx(StackIdx),
+    /// Populate a static upvalue of a thunk by copying it the stack, but do
+    /// when the thunk is finalised (by OpFinalise) rather than immediately.
+    DataDeferredLocal(StackIdx),
+    /// Populate a static upvalue by copying it from the upvalues of an
+    /// enclosing scope.
+    DataUpvalueIdx(UpvalueIdx),
+    /// Populate dynamic upvalues by saving a copy of the with-stack.
+    DataCaptureWith,
+}
diff --git a/tvix/eval/src/pretty_ast.rs b/tvix/eval/src/pretty_ast.rs
new file mode 100644
index 000000000000..5ac115e21c89
--- /dev/null
+++ b/tvix/eval/src/pretty_ast.rs
@@ -0,0 +1,468 @@
+//! Pretty-printed format for the rnix AST representation.
+//!
+//! The AST is serialised into a JSON structure that can then be
+//! printed in either minimised or well-formatted style.
+
+use rnix::ast::{self, AstToken, HasEntry};
+use serde::{ser::SerializeMap, Serialize, Serializer};
+
+pub fn pretty_print_expr(expr: &ast::Expr) -> String {
+    serde_json::ser::to_string_pretty(&SerializeAST(expr))
+        .expect("serializing AST should always succeed")
+}
+
+#[repr(transparent)]
+struct SerializeAST<S>(S);
+
+impl<'a> Serialize for SerializeAST<&'a ast::Apply> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "apply")?;
+        map.serialize_entry("fn", &SerializeAST(&self.0.lambda().unwrap()))?;
+        map.serialize_entry("arg", &SerializeAST(&self.0.argument().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Assert> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "assert")?;
+        map.serialize_entry("condition", &SerializeAST(&self.0.condition().unwrap()))?;
+        map.serialize_entry("body", &SerializeAST(&self.0.body().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Error> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "error")?;
+        map.serialize_entry("node", &self.0.to_string())?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::IfElse> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(4))?;
+        map.serialize_entry("kind", "if_else")?;
+        map.serialize_entry("condition", &SerializeAST(&self.0.condition().unwrap()))?;
+        map.serialize_entry("then_body", &SerializeAST(&self.0.body().unwrap()))?;
+        map.serialize_entry("else_body", &SerializeAST(&self.0.else_body().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Select> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let size = match self.0.default_expr() {
+            Some(_) => 4,
+            None => 3,
+        };
+
+        let mut map = serializer.serialize_map(Some(size))?;
+        map.serialize_entry("kind", "select")?;
+        map.serialize_entry("set", &SerializeAST(&self.0.expr().unwrap()))?;
+        map.serialize_entry("path", &SerializeAST(self.0.attrpath().unwrap()))?;
+
+        if let Some(default) = self.0.default_expr() {
+            map.serialize_entry("default", &SerializeAST(&default))?;
+        }
+
+        map.end()
+    }
+}
+
+impl Serialize for SerializeAST<ast::InterpolPart<String>> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        match &self.0 {
+            ast::InterpolPart::Literal(s) => Serialize::serialize(s, serializer),
+            ast::InterpolPart::Interpolation(node) => {
+                Serialize::serialize(&SerializeAST(&node.expr().unwrap()), serializer)
+            }
+        }
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Str> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "string")?;
+
+        map.serialize_entry(
+            "parts",
+            &self
+                .0
+                .normalized_parts()
+                .into_iter()
+                .map(SerializeAST)
+                .collect::<Vec<_>>(),
+        )?;
+
+        map.end()
+    }
+}
+
+impl Serialize for SerializeAST<ast::InterpolPart<ast::PathContent>> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        match &self.0 {
+            ast::InterpolPart::Literal(p) => Serialize::serialize(p.syntax().text(), serializer),
+            ast::InterpolPart::Interpolation(node) => {
+                Serialize::serialize(&SerializeAST(&node.expr().unwrap()), serializer)
+            }
+        }
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Path> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "path")?;
+
+        map.serialize_entry(
+            "parts",
+            &self.0.parts().map(SerializeAST).collect::<Vec<_>>(),
+        )?;
+
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Literal> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "literal")?;
+
+        match self.0.kind() {
+            ast::LiteralKind::Float(val) => map.serialize_entry("float", &val.value().unwrap()),
+            ast::LiteralKind::Integer(val) => map.serialize_entry("int", &val.value().unwrap()),
+            ast::LiteralKind::Uri(val) => map.serialize_entry("uri", val.syntax().text()),
+        }?;
+
+        map.end()
+    }
+}
+
+impl Serialize for SerializeAST<ast::PatEntry> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(None)?;
+        map.serialize_entry("ident", &SerializeAST(&self.0.ident().unwrap()))?;
+
+        if let Some(default) = self.0.default() {
+            map.serialize_entry("default", &SerializeAST(&default))?;
+        }
+
+        map.end()
+    }
+}
+
+impl Serialize for SerializeAST<ast::Param> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        match &self.0 {
+            ast::Param::Pattern(pat) => {
+                let mut map = serializer.serialize_map(None)?;
+                map.serialize_entry("kind", "formals")?;
+
+                map.serialize_entry(
+                    "entries",
+                    &pat.pat_entries().map(SerializeAST).collect::<Vec<_>>(),
+                )?;
+
+                if let Some(bind) = pat.pat_bind() {
+                    map.serialize_entry("bind", &SerializeAST(&bind.ident().unwrap()))?;
+                }
+
+                map.serialize_entry("ellipsis", &pat.ellipsis_token().is_some())?;
+
+                map.end()
+            }
+
+            ast::Param::IdentParam(node) => {
+                Serialize::serialize(&SerializeAST(&node.ident().unwrap()), serializer)
+            }
+        }
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Lambda> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "lambda")?;
+        map.serialize_entry("param", &SerializeAST(self.0.param().unwrap()))?;
+        map.serialize_entry("body", &SerializeAST(self.0.body().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::LegacyLet> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "legacy_let")?;
+
+        map.serialize_entry(
+            "entries",
+            &self
+                .0
+                .attrpath_values()
+                .map(SerializeAST)
+                .collect::<Vec<_>>(),
+        )?;
+
+        map.serialize_entry(
+            "inherits",
+            &self.0.inherits().map(SerializeAST).collect::<Vec<_>>(),
+        )?;
+
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::LetIn> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "let")?;
+
+        map.serialize_entry(
+            "entries",
+            &self
+                .0
+                .attrpath_values()
+                .map(SerializeAST)
+                .collect::<Vec<_>>(),
+        )?;
+
+        map.serialize_entry(
+            "inherits",
+            &self.0.inherits().map(SerializeAST).collect::<Vec<_>>(),
+        )?;
+
+        map.serialize_entry("body", &SerializeAST(&self.0.body().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::List> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let list = self.0.items().map(SerializeAST).collect::<Vec<_>>();
+
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "list")?;
+        map.serialize_entry("items", &list)?;
+
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::BinOp> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(4))?;
+        map.serialize_entry("kind", "binary_op")?;
+
+        map.serialize_entry(
+            "operator",
+            match self.0.operator().unwrap() {
+                ast::BinOpKind::Concat => "concat",
+                ast::BinOpKind::Update => "update",
+                ast::BinOpKind::Add => "add",
+                ast::BinOpKind::Sub => "sub",
+                ast::BinOpKind::Mul => "mul",
+                ast::BinOpKind::Div => "div",
+                ast::BinOpKind::And => "and",
+                ast::BinOpKind::Equal => "equal",
+                ast::BinOpKind::Implication => "implication",
+                ast::BinOpKind::Less => "less",
+                ast::BinOpKind::LessOrEq => "less_or_eq",
+                ast::BinOpKind::More => "more",
+                ast::BinOpKind::MoreOrEq => "more_or_eq",
+                ast::BinOpKind::NotEqual => "not_equal",
+                ast::BinOpKind::Or => "or",
+            },
+        )?;
+
+        map.serialize_entry("lhs", &SerializeAST(&self.0.lhs().unwrap()))?;
+        map.serialize_entry("rhs", &SerializeAST(&self.0.rhs().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Paren> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "paren")?;
+        map.serialize_entry("expr", &SerializeAST(&self.0.expr().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Root> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "root")?;
+        map.serialize_entry("expr", &SerializeAST(&self.0.expr().unwrap()))?;
+        map.end()
+    }
+}
+
+impl Serialize for SerializeAST<ast::AttrpathValue> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("name", &SerializeAST(self.0.attrpath().unwrap()))?;
+        map.serialize_entry("value", &SerializeAST(self.0.value().unwrap()))?;
+        map.end()
+    }
+}
+
+impl Serialize for SerializeAST<ast::Inherit> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(None)?;
+
+        if let Some(from) = self.0.from() {
+            map.serialize_entry("namespace", &SerializeAST(&from.expr().unwrap()))?;
+        }
+
+        map.serialize_entry(
+            "names",
+            &self.0.attrs().map(SerializeAST).collect::<Vec<_>>(),
+        )?;
+
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::AttrSet> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(None)?;
+        map.serialize_entry("kind", "attrset")?;
+        map.serialize_entry("recursive", &self.0.rec_token().is_some())?;
+
+        map.serialize_entry(
+            "entries",
+            &self
+                .0
+                .attrpath_values()
+                .map(SerializeAST)
+                .collect::<Vec<_>>(),
+        )?;
+
+        map.serialize_entry(
+            "inherits",
+            &self.0.inherits().map(SerializeAST).collect::<Vec<_>>(),
+        )?;
+
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::UnaryOp> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "unary_op")?;
+
+        map.serialize_entry(
+            "operator",
+            match self.0.operator().unwrap() {
+                ast::UnaryOpKind::Invert => "invert",
+                ast::UnaryOpKind::Negate => "negate",
+            },
+        )?;
+
+        map.serialize_entry("expr", &SerializeAST(&self.0.expr().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Ident> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "ident")?;
+        map.serialize_entry("ident", self.0.ident_token().unwrap().text())?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::With> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "with")?;
+        map.serialize_entry("with", &SerializeAST(&self.0.namespace().unwrap()))?;
+        map.serialize_entry("body", &SerializeAST(&self.0.body().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Dynamic> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "dynamic")?;
+        map.serialize_entry("expr", &SerializeAST(&self.0.expr().unwrap()))?;
+        map.end()
+    }
+}
+
+impl Serialize for SerializeAST<ast::Attr> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        match &self.0 {
+            ast::Attr::Ident(ident) => Serialize::serialize(&SerializeAST(ident), serializer),
+            ast::Attr::Dynamic(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Attr::Str(node) => Serialize::serialize(&SerializeAST(node), serializer),
+        }
+    }
+}
+
+impl Serialize for SerializeAST<ast::Attrpath> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(2))?;
+        map.serialize_entry("kind", "attrpath")?;
+
+        map.serialize_entry(
+            "path",
+            &self.0.attrs().map(SerializeAST).collect::<Vec<_>>(),
+        )?;
+
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::HasAttr> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        let mut map = serializer.serialize_map(Some(3))?;
+        map.serialize_entry("kind", "has_attr")?;
+        map.serialize_entry("expr", &SerializeAST(&self.0.expr().unwrap()))?;
+        map.serialize_entry("attrpath", &SerializeAST(self.0.attrpath().unwrap()))?;
+        map.end()
+    }
+}
+
+impl<'a> Serialize for SerializeAST<&'a ast::Expr> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        match self.0 {
+            ast::Expr::Apply(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Assert(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Error(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::IfElse(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Select(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Str(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Path(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Literal(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Lambda(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::LegacyLet(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::LetIn(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::List(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::BinOp(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Paren(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Root(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::AttrSet(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::UnaryOp(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::Ident(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::With(node) => Serialize::serialize(&SerializeAST(node), serializer),
+            ast::Expr::HasAttr(node) => Serialize::serialize(&SerializeAST(node), serializer),
+        }
+    }
+}
+
+impl Serialize for SerializeAST<ast::Expr> {
+    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        SerializeAST(&self.0).serialize(serializer)
+    }
+}
diff --git a/tvix/eval/src/properties.rs b/tvix/eval/src/properties.rs
new file mode 100644
index 000000000000..45c1cdfce911
--- /dev/null
+++ b/tvix/eval/src/properties.rs
@@ -0,0 +1,164 @@
+//! Macros that generate proptest test suites checking laws of stdlib traits
+
+/// Generate a suite of tests to check the laws of the [`Eq`] impl for the given type
+macro_rules! eq_laws {
+    ($ty: ty) => {
+        eq_laws!(
+            #[strategy(::proptest::arbitrary::any::<$ty>())]
+            $ty,
+            Default::default()
+        );
+    };
+    ($ty: ty, $config: expr) => {
+        eq_laws!(
+            #[strategy(::proptest::arbitrary::any::<$ty>())]
+            $ty,
+            $config
+        );
+    };
+    (#[$meta: meta] $ty: ty, $config: expr) => {
+        #[allow(clippy::eq_op)]
+        mod eq {
+            use test_strategy::proptest;
+
+            use super::*;
+
+            #[proptest($config)]
+            fn reflexive(#[$meta] x: $ty) {
+                assert!(x == x);
+            }
+
+            #[proptest($config)]
+            fn symmetric(#[$meta] x: $ty, #[$meta] y: $ty) {
+                assert_eq!(x == y, y == x);
+            }
+
+            #[proptest($config)]
+            fn transitive(#[$meta] x: $ty, #[$meta] y: $ty, #[$meta] z: $ty) {
+                if x == y && y == z {
+                    assert!(x == z);
+                }
+            }
+        }
+    };
+}
+
+/// Generate a suite of tests to check the laws of the [`Ord`] impl for the given type
+macro_rules! ord_laws {
+    ($ty: ty) => {
+        ord_laws!(
+            #[strategy(::proptest::arbitrary::any::<$ty>())]
+            $ty,
+            Default::default()
+        );
+    };
+    ($ty: ty, $config: expr) => {
+        ord_laws!(
+            #[strategy(::proptest::arbitrary::any::<$ty>())]
+            $ty,
+            $config
+        );
+    };
+    (#[$meta: meta] $ty: ty, $config: expr) => {
+        mod ord {
+            use test_strategy::proptest;
+
+            use super::*;
+
+            #[proptest($config)]
+            fn partial_cmp_matches_cmp(#[$meta] x: $ty, #[$meta] y: $ty) {
+                assert_eq!(x.partial_cmp(&y), Some(x.cmp(&y)));
+            }
+
+            #[proptest($config)]
+            fn dual(#[$meta] x: $ty, #[$meta] y: $ty) {
+                if x < y {
+                    assert!(y > x);
+                }
+                if y < x {
+                    assert!(x > y);
+                }
+            }
+
+            #[proptest($config)]
+            fn le_transitive(#[$meta] x: $ty, #[$meta] y: $ty, #[$meta] z: $ty) {
+                if x < y && y < z {
+                    assert!(x < z)
+                }
+            }
+
+            #[proptest($config)]
+            fn gt_transitive(#[$meta] x: $ty, #[$meta] y: $ty, #[$meta] z: $ty) {
+                if x > y && y > z {
+                    assert!(x > z)
+                }
+            }
+
+            #[proptest($config)]
+            fn trichotomy(#[$meta] x: $ty, #[$meta] y: $ty) {
+                let less = x < y;
+                let greater = x > y;
+                let eq = x == y;
+
+                if less {
+                    assert!(!greater);
+                    assert!(!eq);
+                }
+
+                if greater {
+                    assert!(!less);
+                    assert!(!eq);
+                }
+
+                if eq {
+                    assert!(!less);
+                    assert!(!greater);
+                }
+            }
+        }
+    };
+}
+
+/// Generate a test to check the laws of the [`Hash`] impl for the given type
+macro_rules! hash_laws {
+    ($ty: ty) => {
+        hash_laws!(
+            #[strategy(::proptest::arbitrary::any::<$ty>())]
+            $ty,
+            Default::default()
+        );
+    };
+    ($ty: ty, $config: expr) => {
+        hash_laws!(
+            #[strategy(::proptest::arbitrary::any::<$ty>())]
+            $ty,
+            $config
+        );
+    };
+    (#[$meta: meta] $ty: ty, $config: expr) => {
+        mod hash {
+            use test_strategy::proptest;
+
+            use super::*;
+
+            #[proptest($config)]
+            fn matches_eq(#[$meta] x: $ty, #[$meta] y: $ty) {
+                let hash = |x: &$ty| {
+                    use std::hash::Hasher;
+
+                    let mut hasher = ::std::collections::hash_map::DefaultHasher::new();
+                    x.hash(&mut hasher);
+                    hasher.finish()
+                };
+
+                if x == y {
+                    assert_eq!(hash(&x), hash(&y));
+                }
+            }
+        }
+    };
+}
+
+pub(crate) use eq_laws;
+pub(crate) use hash_laws;
+pub(crate) use ord_laws;
diff --git a/tvix/eval/src/source.rs b/tvix/eval/src/source.rs
new file mode 100644
index 000000000000..649679536080
--- /dev/null
+++ b/tvix/eval/src/source.rs
@@ -0,0 +1,63 @@
+//! This module contains utilities for dealing with the codemap that
+//! needs to be carried across different compiler instantiations in an
+//! evaluation.
+//!
+//! The data type `SourceCode` should be carried through all relevant
+//! places instead of copying the codemap structures directly.
+
+use std::{
+    cell::{Ref, RefCell, RefMut},
+    rc::Rc,
+    sync::Arc,
+};
+
+use codemap::{CodeMap, Span};
+
+/// Tracks all source code in a Tvix evaluation for accurate error
+/// reporting.
+#[derive(Clone, Debug)]
+pub struct SourceCode(Rc<RefCell<CodeMap>>);
+
+impl SourceCode {
+    /// Create a new SourceCode instance.
+    pub fn new() -> Self {
+        SourceCode(Rc::new(RefCell::new(CodeMap::new())))
+    }
+
+    /// Access a read-only reference to the codemap.
+    pub fn codemap(&self) -> Ref<CodeMap> {
+        self.0.borrow()
+    }
+
+    /// Access a writable reference to the codemap.
+    fn codemap_mut(&self) -> RefMut<CodeMap> {
+        self.0.borrow_mut()
+    }
+
+    /// Add a file to the codemap. The returned Arc is managed by the
+    /// codemap internally and can be used like a normal reference.
+    pub fn add_file(&self, name: String, code: String) -> Arc<codemap::File> {
+        self.codemap_mut().add_file(name, code)
+    }
+
+    /// Retrieve the line number of the given span. If it spans
+    /// multiple lines, the first line will be returned.
+    pub fn get_line(&self, span: Span) -> usize {
+        // lines are 0-indexed in the codemap, but users probably want
+        // real line numbers
+        self.codemap().look_up_span(span).begin.line + 1
+    }
+
+    /// Returns the literal source slice of the given span.
+    pub fn source_slice(&self, span: Span) -> Ref<str> {
+        Ref::map(self.codemap(), |c| {
+            c.find_file(span.low()).source_slice(span)
+        })
+    }
+
+    /// Returns the reference to the file structure that a given span
+    /// is in.
+    pub fn get_file(&self, span: Span) -> Arc<codemap::File> {
+        self.codemap().look_up_span(span).file
+    }
+}
diff --git a/tvix/eval/src/spans.rs b/tvix/eval/src/spans.rs
new file mode 100644
index 000000000000..c0130a665428
--- /dev/null
+++ b/tvix/eval/src/spans.rs
@@ -0,0 +1,121 @@
+//! Utilities for dealing with span tracking in the compiler and in
+//! error reporting.
+
+use crate::opcode::CodeIdx;
+use crate::value::Lambda;
+use codemap::{File, Span};
+use rnix::ast;
+use rowan::ast::AstNode;
+use std::rc::Rc;
+
+/// Helper struct to carry information required for making a span, but
+/// without actually performing the (expensive) span lookup.
+///
+/// This is used for tracking spans across thunk boundaries, as they
+/// are frequently instantiated but spans are only used in error or
+/// warning cases.
+#[derive(Clone, Debug)]
+pub enum LightSpan {
+    /// The span has already been computed and can just be used right
+    /// away.
+    Actual { span: Span },
+
+    /// The span needs to be computed from the provided data, but only
+    /// when it is required.
+    Delayed { lambda: Rc<Lambda>, offset: CodeIdx },
+}
+
+impl LightSpan {
+    pub fn new_delayed(lambda: Rc<Lambda>, offset: CodeIdx) -> Self {
+        Self::Delayed { lambda, offset }
+    }
+
+    pub fn new_actual(span: Span) -> Self {
+        Self::Actual { span }
+    }
+
+    pub fn span(&self) -> Span {
+        match self {
+            LightSpan::Actual { span } => *span,
+            LightSpan::Delayed { lambda, offset } => lambda.chunk.get_span(*offset),
+        }
+    }
+}
+
+impl From<Span> for LightSpan {
+    fn from(span: Span) -> Self {
+        LightSpan::Actual { span }
+    }
+}
+
+/// Trait implemented by all types from which we can retrieve a span.
+pub trait ToSpan {
+    fn span_for(&self, file: &File) -> Span;
+}
+
+impl ToSpan for Span {
+    fn span_for(&self, _: &File) -> Span {
+        *self
+    }
+}
+
+impl ToSpan for rnix::TextRange {
+    fn span_for(&self, file: &File) -> Span {
+        file.span
+            .subspan(u32::from(self.start()) as u64, u32::from(self.end()) as u64)
+    }
+}
+
+impl ToSpan for rnix::SyntaxToken {
+    fn span_for(&self, file: &File) -> Span {
+        self.text_range().span_for(file)
+    }
+}
+
+impl ToSpan for rnix::SyntaxNode {
+    fn span_for(&self, file: &File) -> Span {
+        self.text_range().span_for(file)
+    }
+}
+
+/// Generates a `ToSpan` implementation for a type implementing
+/// `rowan::AstNode`. This is impossible to do as a blanket
+/// implementation because `rustc` forbids these implementations for
+/// traits from third-party crates due to a belief that semantic
+/// versioning truly could work (it doesn't).
+macro_rules! expr_to_span {
+    ( $type:path ) => {
+        impl ToSpan for $type {
+            fn span_for(&self, file: &File) -> Span {
+                self.syntax().span_for(file)
+            }
+        }
+    };
+}
+
+expr_to_span!(ast::Expr);
+expr_to_span!(ast::Apply);
+expr_to_span!(ast::Assert);
+expr_to_span!(ast::Attr);
+expr_to_span!(ast::AttrSet);
+expr_to_span!(ast::Attrpath);
+expr_to_span!(ast::AttrpathValue);
+expr_to_span!(ast::BinOp);
+expr_to_span!(ast::HasAttr);
+expr_to_span!(ast::Ident);
+expr_to_span!(ast::IdentParam);
+expr_to_span!(ast::IfElse);
+expr_to_span!(ast::Inherit);
+expr_to_span!(ast::Interpol);
+expr_to_span!(ast::Lambda);
+expr_to_span!(ast::LegacyLet);
+expr_to_span!(ast::LetIn);
+expr_to_span!(ast::List);
+expr_to_span!(ast::Literal);
+expr_to_span!(ast::PatBind);
+expr_to_span!(ast::Path);
+expr_to_span!(ast::Pattern);
+expr_to_span!(ast::Select);
+expr_to_span!(ast::Str);
+expr_to_span!(ast::UnaryOp);
+expr_to_span!(ast::With);
diff --git a/tvix/eval/src/systems.rs b/tvix/eval/src/systems.rs
new file mode 100644
index 000000000000..16386cb9e0ad
--- /dev/null
+++ b/tvix/eval/src/systems.rs
@@ -0,0 +1,351 @@
+/// true iff the argument is recognized by cppnix as the second
+/// coordinate of a "nix double"
+fn is_second_coordinate(x: &str) -> bool {
+    matches!(x, "linux" | "darwin" | "netbsd" | "openbsd" | "freebsd")
+}
+
+/// This function takes an llvm triple (which may have three or four
+/// components, separated by dashes) and returns the "best"
+/// approximation as a nix double, where "best" is currently defined
+/// as "however cppnix handles it".
+pub fn llvm_triple_to_nix_double(llvm_triple: &str) -> String {
+    let parts: Vec<&str> = llvm_triple.split('-').collect();
+    let cpu = match parts[0] {
+        "armv6" => "armv6l", // cppnix appends an "l" to armv6
+        "armv7" => "armv7l", // cppnix appends an "l" to armv7
+        x => match x.as_bytes() {
+            [b'i', _, b'8', b'6'] => "i686", // cppnix glob-matches against i*86
+            _ => x,
+        },
+    };
+    let os = match parts[1..] {
+        [_vendor, kernel, _environment] if is_second_coordinate(kernel) => kernel,
+        [_vendor, kernel] if is_second_coordinate(kernel) => kernel,
+        [kernel, _environment] if is_second_coordinate(kernel) => kernel,
+
+        // Rustc uses wasm32-unknown-unknown, which is rejected by
+        // config.sub, for wasm-in-the-browser environments.  Rustc
+        // should be using wasm32-unknown-none, which config.sub
+        // accepts.  Hopefully the rustc people will change their
+        // triple before stabilising this triple.  In the meantime,
+        // we fix it here in order to unbreak tvixbolt.
+        //
+        // https://doc.rust-lang.org/beta/nightly-rustc/rustc_target/spec/wasm32_unknown_unknown/index.html
+        ["unknown", "unknown"] if cpu == "wasm32" => "none",
+
+        _ => panic!("unrecognized triple {llvm_triple}"),
+    };
+    format!("{cpu}-{os}")
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    #[test]
+    fn test_systems() {
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64-unknown-linux-gnu"),
+            "aarch64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("i686-unknown-linux-gnu"),
+            "i686-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("x86_64-apple-darwin"),
+            "x86_64-darwin"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("x86_64-unknown-linux-gnu"),
+            "x86_64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64-apple-darwin"),
+            "aarch64-darwin"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64-unknown-linux-musl"),
+            "aarch64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("arm-unknown-linux-gnueabi"),
+            "arm-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("arm-unknown-linux-gnueabihf"),
+            "arm-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-unknown-linux-gnueabihf"),
+            "armv7l-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips-unknown-linux-gnu"),
+            "mips-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips64-unknown-linux-gnuabi64"),
+            "mips64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips64-unknown-linux-gnuabin32"),
+            "mips64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips64el-unknown-linux-gnuabi64"),
+            "mips64el-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips64el-unknown-linux-gnuabin32"),
+            "mips64el-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mipsel-unknown-linux-gnu"),
+            "mipsel-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc-unknown-linux-gnu"),
+            "powerpc-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc64-unknown-linux-gnu"),
+            "powerpc64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc64le-unknown-linux-gnu"),
+            "powerpc64le-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("s390x-unknown-linux-gnu"),
+            "s390x-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("x86_64-unknown-linux-musl"),
+            "x86_64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("x86_64-unknown-netbsd"),
+            "x86_64-netbsd"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64-linux-android"),
+            "aarch64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("arm-linux-androideabi"),
+            "arm-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("arm-unknown-linux-musleabi"),
+            "arm-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("arm-unknown-linux-musleabihf"),
+            "arm-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv5te-unknown-linux-gnueabi"),
+            "armv5te-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv5te-unknown-linux-musleabi"),
+            "armv5te-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-linux-androideabi"),
+            "armv7l-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-unknown-linux-gnueabi"),
+            "armv7l-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-unknown-linux-musleabi"),
+            "armv7l-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-unknown-linux-musleabihf"),
+            "armv7l-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("i586-unknown-linux-gnu"),
+            "i686-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("i586-unknown-linux-musl"),
+            "i686-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("i686-linux-android"),
+            "i686-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("i686-unknown-linux-musl"),
+            "i686-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips-unknown-linux-musl"),
+            "mips-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips64-unknown-linux-muslabi64"),
+            "mips64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips64el-unknown-linux-muslabi64"),
+            "mips64el-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mipsel-unknown-linux-musl"),
+            "mipsel-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("sparc64-unknown-linux-gnu"),
+            "sparc64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("thumbv7neon-linux-androideabi"),
+            "thumbv7neon-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("thumbv7neon-unknown-linux-gnueabihf"),
+            "thumbv7neon-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("x86_64-linux-android"),
+            "x86_64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("x86_64-unknown-linux-gnux32"),
+            "x86_64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64-unknown-linux-gnu_ilp32"),
+            "aarch64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64-unknown-netbsd"),
+            "aarch64-netbsd"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64_be-unknown-linux-gnu_ilp32"),
+            "aarch64_be-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("aarch64_be-unknown-linux-gnu"),
+            "aarch64_be-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armeb-unknown-linux-gnueabi"),
+            "armeb-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv4t-unknown-linux-gnueabi"),
+            "armv4t-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv6-unknown-netbsd-eabihf"),
+            "armv6l-netbsd"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-unknown-linux-uclibceabi"),
+            "armv7l-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-unknown-linux-uclibceabihf"),
+            "armv7l-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("armv7-unknown-netbsd-eabihf"),
+            "armv7l-netbsd"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("hexagon-unknown-linux-musl"),
+            "hexagon-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("i686-unknown-netbsd"),
+            "i686-netbsd"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("m68k-unknown-linux-gnu"),
+            "m68k-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips-unknown-linux-uclibc"),
+            "mips-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mips64-openwrt-linux-musl"),
+            "mips64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mipsel-unknown-linux-uclibc"),
+            "mipsel-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mipsisa32r6-unknown-linux-gnu"),
+            "mipsisa32r6-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mipsisa32r6el-unknown-linux-gnu"),
+            "mipsisa32r6el-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mipsisa64r6-unknown-linux-gnuabi64"),
+            "mipsisa64r6-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("mipsisa64r6el-unknown-linux-gnuabi64"),
+            "mipsisa64r6el-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc-unknown-linux-gnuspe"),
+            "powerpc-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc-unknown-linux-musl"),
+            "powerpc-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc-unknown-netbsd"),
+            "powerpc-netbsd"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc64-unknown-linux-musl"),
+            "powerpc64-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("powerpc64le-unknown-linux-musl"),
+            "powerpc64le-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("riscv32gc-unknown-linux-gnu"),
+            "riscv32gc-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("riscv32gc-unknown-linux-musl"),
+            "riscv32gc-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("riscv64gc-unknown-linux-musl"),
+            "riscv64gc-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("s390x-unknown-linux-musl"),
+            "s390x-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("sparc-unknown-linux-gnu"),
+            "sparc-linux"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("sparc64-unknown-netbsd"),
+            "sparc64-netbsd"
+        );
+        assert_eq!(
+            llvm_triple_to_nix_double("thumbv7neon-unknown-linux-musleabihf"),
+            "thumbv7neon-linux"
+        );
+    }
+}
diff --git a/tvix/eval/src/test_utils.rs b/tvix/eval/src/test_utils.rs
new file mode 100644
index 000000000000..a7d1c3f96899
--- /dev/null
+++ b/tvix/eval/src/test_utils.rs
@@ -0,0 +1,8 @@
+use codemap::CodeMap;
+
+/// Create a dummy [`codemap::Span`] for use in tests
+pub(crate) fn dummy_span() -> codemap::Span {
+    let mut codemap = CodeMap::new();
+    let file = codemap.add_file("<dummy>".to_owned(), "<dummy>".to_owned());
+    file.span
+}
diff --git a/tvix/eval/src/tests/mod.rs b/tvix/eval/src/tests/mod.rs
new file mode 100644
index 000000000000..f800baf05018
--- /dev/null
+++ b/tvix/eval/src/tests/mod.rs
@@ -0,0 +1,181 @@
+use builtin_macros::builtins;
+use pretty_assertions::assert_eq;
+use test_generator::test_resources;
+
+/// Module for one-off tests which do not follow the rest of the
+/// test layout.
+mod one_offs;
+
+#[builtins]
+mod mock_builtins {
+    //! Builtins which are required by language tests, but should not
+    //! actually exist in //tvix/eval.
+    use crate::generators::GenCo;
+    use crate::*;
+    use genawaiter::rc::Gen;
+
+    #[builtin("derivation")]
+    async fn builtin_derivation(co: GenCo, input: Value) -> Result<Value, ErrorKind> {
+        let input = input.to_attrs()?;
+        let attrs = input.update(NixAttrs::from_iter(
+            [
+                (
+                    "outPath",
+                    "/nix/store/00000000000000000000000000000000-mock",
+                ),
+                (
+                    "drvPath",
+                    "/nix/store/00000000000000000000000000000000-mock.drv",
+                ),
+                ("type", "derivation"),
+            ]
+            .into_iter(),
+        ));
+
+        Ok(Value::Attrs(Box::new(attrs)))
+    }
+}
+
+fn eval_test(code_path: &str, expect_success: bool) {
+    let base = code_path
+        .strip_suffix("nix")
+        .expect("test files always end in .nix");
+    let exp_path = format!("{}exp", base);
+    let exp_xml_path = std::path::PathBuf::from(format!("{}exp.xml", base));
+
+    let code = std::fs::read_to_string(code_path).expect("should be able to read test code");
+
+    if exp_xml_path.exists() {
+        // We can't test them at the moment because we don't have XML output yet.
+        // Checking for success / failure only is a bit disingenious.
+        return;
+    }
+
+    let mut eval = crate::Evaluation::new_impure(&code, Some(code_path.into()));
+    eval.strict = true;
+    eval.builtins.extend(mock_builtins::builtins());
+
+    let result = eval.evaluate();
+
+    if expect_success && !result.errors.is_empty() {
+        panic!(
+            "{code_path}: evaluation of eval-okay test should succeed, but failed with {:?}",
+            result.errors,
+        );
+    }
+
+    if !expect_success && !result.errors.is_empty() {
+        return;
+    }
+
+    let result_str = result.value.unwrap().to_string();
+
+    if let Ok(exp) = std::fs::read_to_string(exp_path) {
+        if expect_success {
+            assert_eq!(
+                result_str,
+                exp.trim(),
+                "{code_path}: result value representation (left) must match expectation (right)"
+            );
+        } else {
+            assert_ne!(
+                result_str,
+                exp.trim(),
+                "{code_path}: test passed unexpectedly!  consider moving it out of notyetpassing"
+            );
+        }
+    } else if expect_success {
+        panic!("{code_path}: should be able to read test expectation");
+    } else {
+        panic!(
+            "{code_path}: test should have failed, but succeeded with output {}",
+            result_str
+        );
+    }
+}
+
+// identity-* tests contain Nix code snippets which should evaluate to
+// themselves exactly (i.e. literals).
+#[test_resources("src/tests/tvix_tests/identity-*.nix")]
+fn identity(code_path: &str) {
+    let code = std::fs::read_to_string(code_path).expect("should be able to read test code");
+
+    let mut eval = crate::Evaluation::new(&code, None);
+    eval.strict = true;
+    eval.io_handle = Box::new(crate::StdIO);
+
+    let result = eval.evaluate();
+    assert!(
+        result.errors.is_empty(),
+        "evaluation of identity test failed: {:?}",
+        result.errors
+    );
+
+    let result_str = result.value.unwrap().to_string();
+
+    assert_eq!(
+        result_str,
+        code.trim(),
+        "result value representation (left) must match expectation (right)"
+    )
+}
+
+// eval-okay-* tests contain a snippet of Nix code, and an expectation
+// of the produced string output of the evaluator.
+//
+// These evaluations are always supposed to succeed, i.e. all snippets
+// are guaranteed to be valid Nix code.
+#[test_resources("src/tests/tvix_tests/eval-okay-*.nix")]
+fn eval_okay(code_path: &str) {
+    eval_test(code_path, true)
+}
+
+// eval-okay-* tests from the original Nix test suite.
+#[cfg(feature = "nix_tests")]
+#[test_resources("src/tests/nix_tests/eval-okay-*.nix")]
+fn nix_eval_okay(code_path: &str) {
+    eval_test(code_path, true)
+}
+
+// eval-okay-* tests from the original Nix test suite which do not yet pass for tvix
+//
+// Eventually there will be none of these left, and this function
+// will disappear :) Until then, to run these tests, use `cargo test
+// --features expected_failures`.
+//
+// Please don't submit failing tests unless they're in
+// notyetpassing; this makes the test suite much more useful for
+// regression testing, since there should always be zero non-ignored
+// failing tests.
+//
+// Unfortunately test_generator is unmaintained, so the PRs to make
+// it understand #[ignored] has been sitting for two years, so we
+// can't use `cargo test --include-ignored`, which is the normal way
+// of handling this situation.
+//
+//   https://github.com/frehberg/test-generator/pull/10
+//   https://github.com/frehberg/test-generator/pull/8
+#[test_resources("src/tests/nix_tests/notyetpassing/eval-okay-*.nix")]
+fn nix_eval_okay_currently_failing(code_path: &str) {
+    eval_test(code_path, false)
+}
+
+#[test_resources("src/tests/tvix_tests/notyetpassing/eval-okay-*.nix")]
+fn eval_okay_currently_failing(code_path: &str) {
+    eval_test(code_path, false)
+}
+
+// eval-fail-* tests contain a snippet of Nix code, which is
+// expected to fail evaluation.  The exact type of failure
+// (assertion, parse error, etc) is not currently checked.
+#[test_resources("src/tests/tvix_tests/eval-fail-*.nix")]
+fn eval_fail(code_path: &str) {
+    eval_test(code_path, false)
+}
+
+// eval-fail-* tests from the original Nix test suite.
+#[cfg(feature = "nix_tests")]
+#[test_resources("src/tests/nix_tests/eval-fail-*.nix")]
+fn nix_eval_fail(code_path: &str) {
+    eval_test(code_path, false)
+}
diff --git a/tvix/eval/src/tests/nix_tests/README.md b/tvix/eval/src/tests/nix_tests/README.md
new file mode 100644
index 000000000000..357f3547da18
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/README.md
@@ -0,0 +1,8 @@
+These test definitions are taken from the Nix 2.3 code base, they can
+be found upstream at:
+
+  https://github.com/NixOS/nix/tree/2.3.16/tests/lang
+
+These tests follow the licensing directions of Nix 2.3 itself:
+
+  https://github.com/NixOS/nix/blob/2.3.16/COPYING
diff --git a/tvix/eval/src/tests/nix_tests/binary-data b/tvix/eval/src/tests/nix_tests/binary-data
new file mode 100644
index 000000000000..06d740502001
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/binary-data
Binary files differdiff --git a/tvix/eval/src/tests/nix_tests/data b/tvix/eval/src/tests/nix_tests/data
new file mode 100644
index 000000000000..257cc5642cb1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/data
@@ -0,0 +1 @@
+foo
diff --git a/tvix/eval/src/tests/nix_tests/dir1/a.nix b/tvix/eval/src/tests/nix_tests/dir1/a.nix
new file mode 100644
index 000000000000..231f150c579c
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir1/a.nix
@@ -0,0 +1 @@
+"a"
diff --git a/tvix/eval/src/tests/nix_tests/dir2/a.nix b/tvix/eval/src/tests/nix_tests/dir2/a.nix
new file mode 100644
index 000000000000..170df520ab68
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir2/a.nix
@@ -0,0 +1 @@
+"X"
diff --git a/tvix/eval/src/tests/nix_tests/dir2/b.nix b/tvix/eval/src/tests/nix_tests/dir2/b.nix
new file mode 100644
index 000000000000..19010cc35ca6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir2/b.nix
@@ -0,0 +1 @@
+"b"
diff --git a/tvix/eval/src/tests/nix_tests/dir3/a.nix b/tvix/eval/src/tests/nix_tests/dir3/a.nix
new file mode 100644
index 000000000000..170df520ab68
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir3/a.nix
@@ -0,0 +1 @@
+"X"
diff --git a/tvix/eval/src/tests/nix_tests/dir3/b.nix b/tvix/eval/src/tests/nix_tests/dir3/b.nix
new file mode 100644
index 000000000000..170df520ab68
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir3/b.nix
@@ -0,0 +1 @@
+"X"
diff --git a/tvix/eval/src/tests/nix_tests/dir3/c.nix b/tvix/eval/src/tests/nix_tests/dir3/c.nix
new file mode 100644
index 000000000000..cdf158597eef
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir3/c.nix
@@ -0,0 +1 @@
+"c"
diff --git a/tvix/eval/src/tests/nix_tests/dir4/a.nix b/tvix/eval/src/tests/nix_tests/dir4/a.nix
new file mode 100644
index 000000000000..170df520ab68
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir4/a.nix
@@ -0,0 +1 @@
+"X"
diff --git a/tvix/eval/src/tests/nix_tests/dir4/c.nix b/tvix/eval/src/tests/nix_tests/dir4/c.nix
new file mode 100644
index 000000000000..170df520ab68
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/dir4/c.nix
@@ -0,0 +1 @@
+"X"
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-abort.nix b/tvix/eval/src/tests/nix_tests/eval-fail-abort.nix
new file mode 100644
index 000000000000..75c51bceb540
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-abort.nix
@@ -0,0 +1 @@
+if true then abort "this should fail" else 1
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-assert.nix b/tvix/eval/src/tests/nix_tests/eval-fail-assert.nix
new file mode 100644
index 000000000000..3b7a1e8bf0c2
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-assert.nix
@@ -0,0 +1,5 @@
+let {
+  x = arg: assert arg == "y"; 123;
+
+  body = x "x";
+}
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-1.nix b/tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-1.nix
new file mode 100644
index 000000000000..ffe9c983c26b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-1.nix
@@ -0,0 +1 @@
+"${x: x}"
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-3.nix b/tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-3.nix
new file mode 100644
index 000000000000..65b9d4f505b1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-bad-antiquote-3.nix
@@ -0,0 +1 @@
+''${x: x}''
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-blackhole.nix-disabled b/tvix/eval/src/tests/nix_tests/eval-fail-blackhole.nix-disabled
new file mode 100644
index 000000000000..81133b511c95
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-blackhole.nix-disabled
@@ -0,0 +1,5 @@
+let {
+  body = x;
+  x = y;
+  y = x;
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-deepseq.nix b/tvix/eval/src/tests/nix_tests/eval-fail-deepseq.nix
new file mode 100644
index 000000000000..9baa49b063ec
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-deepseq.nix
@@ -0,0 +1 @@
+builtins.deepSeq { x = abort "foo"; } 456
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-float.nix b/tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-float.nix
new file mode 100644
index 000000000000..82dd6873218e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-float.nix
@@ -0,0 +1 @@
+1.0 / 0.0
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-int.nix b/tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-int.nix
new file mode 100644
index 000000000000..72dca4d5e478
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-division-by-zero-int.nix
@@ -0,0 +1 @@
+1 / 0
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-hashfile-missing.nix b/tvix/eval/src/tests/nix_tests/eval-fail-hashfile-missing.nix
new file mode 100644
index 000000000000..ce098b82380a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-hashfile-missing.nix
@@ -0,0 +1,5 @@
+let
+  paths = [ ./this-file-is-definitely-not-there-7392097 "/and/neither/is/this/37293620" ];
+in
+  toString (builtins.concatLists (map (hash: map (builtins.hashFile hash) paths) ["md5" "sha1" "sha256" "sha512"]))
+
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-missing-arg.nix b/tvix/eval/src/tests/nix_tests/eval-fail-missing-arg.nix
new file mode 100644
index 000000000000..c4be9797c534
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-missing-arg.nix
@@ -0,0 +1 @@
+({x, y, z}: x + y + z) {x = "foo"; z = "bar";}
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-path-slash.nix b/tvix/eval/src/tests/nix_tests/eval-fail-path-slash.nix
new file mode 100644
index 000000000000..8c2e104c788f
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-path-slash.nix
@@ -0,0 +1,6 @@
+# Trailing slashes in paths are not allowed.
+# This restriction could be lifted sometime,
+# for example if we make '/' a path concatenation operator.
+# See https://github.com/NixOS/nix/issues/1138
+# and https://nixos.org/nix-dev/2016-June/020829.html
+/nix/store/
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-remove.nix b/tvix/eval/src/tests/nix_tests/eval-fail-remove.nix
new file mode 100644
index 000000000000..539e0eb0a6f6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-remove.nix
@@ -0,0 +1,5 @@
+let {
+  attrs = {x = 123; y = 456;};
+
+  body = (removeAttrs attrs ["x"]).x;
+}
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-seq.nix b/tvix/eval/src/tests/nix_tests/eval-fail-seq.nix
new file mode 100644
index 000000000000..cddbbfd3261e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-seq.nix
@@ -0,0 +1 @@
+builtins.seq (abort "foo") 2
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-substring.nix b/tvix/eval/src/tests/nix_tests/eval-fail-substring.nix
new file mode 100644
index 000000000000..f37c2bc0a160
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-substring.nix
@@ -0,0 +1 @@
+builtins.substring (builtins.sub 0 1) 1 "x"
diff --git a/tvix/eval/src/tests/nix_tests/eval-fail-to-path.nix b/tvix/eval/src/tests/nix_tests/eval-fail-to-path.nix
new file mode 100644
index 000000000000..5e322bc31369
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-fail-to-path.nix
@@ -0,0 +1 @@
+builtins.toPath "foo/bar"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-any-all.exp b/tvix/eval/src/tests/nix_tests/eval-okay-any-all.exp
new file mode 100644
index 000000000000..eb273f45b2a6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-any-all.exp
@@ -0,0 +1 @@
+[ false false true true true true false true ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-any-all.nix b/tvix/eval/src/tests/nix_tests/eval-okay-any-all.nix
new file mode 100644
index 000000000000..a3f26ea2aa83
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-any-all.nix
@@ -0,0 +1,11 @@
+with builtins;
+
+[ (any (x: x == 1) [])
+  (any (x: x == 1) [2 3 4])
+  (any (x: x == 1) [1 2 3 4])
+  (any (x: x == 1) [4 3 2 1])
+  (all (x: x == 1) [])
+  (all (x: x == 1) [1])
+  (all (x: x == 1) [1 2 3])
+  (all (x: x == 1) [1 1 1])
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.exp b/tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.exp
new file mode 100644
index 000000000000..5c54d10b7b47
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.exp
@@ -0,0 +1 @@
+2216
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.nix b/tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.nix
new file mode 100644
index 000000000000..7e9e6a0b666e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-arithmetic.nix
@@ -0,0 +1,59 @@
+with import ./lib.nix;
+
+let {
+
+  /* Supposedly tail recursive version:
+
+  range_ = accum: first: last:
+    if first == last then ([first] ++ accum)
+    else range_ ([first] ++ accum) (builtins.add first 1) last;
+
+  range = range_ [];
+  */
+
+  x = 12;
+
+  err = abort "urgh";
+
+  body = sum
+    [ (sum (range 1 50))
+      (123 + 456)
+      (0 + -10 + -(-11) + -x)
+      (10 - 7 - -2)
+      (10 - (6 - -1))
+      (10 - 1 + 2)
+      (3 * 4 * 5)
+      (56088 / 123 / 2)
+      (3 + 4 * const 5 0 - 6 / id 2)
+
+      (builtins.bitAnd 12 10) # 0b1100 & 0b1010 =  8
+      (builtins.bitOr  12 10) # 0b1100 | 0b1010 = 14
+      (builtins.bitXor 12 10) # 0b1100 ^ 0b1010 =  6
+
+      (if 3 < 7 then 1 else err)
+      (if 7 < 3 then err else 1)
+      (if 3 < 3 then err else 1)
+
+      (if 3 <= 7 then 1 else err)
+      (if 7 <= 3 then err else 1)
+      (if 3 <= 3 then 1 else err)
+
+      (if 3 > 7 then err else 1)
+      (if 7 > 3 then 1 else err)
+      (if 3 > 3 then err else 1)
+
+      (if 3 >= 7 then err else 1)
+      (if 7 >= 3 then 1 else err)
+      (if 3 >= 3 then 1 else err)
+
+      (if 2 > 1 == 1 < 2 then 1 else err)
+      (if 1 + 2 * 3 >= 7 then 1 else err)
+      (if 1 + 2 * 3 < 7 then err else 1)
+
+      # Not integer, but so what.
+      (if "aa" < "ab" then 1 else err)
+      (if "aa" < "aa" then err else 1)
+      (if "foo" < "foobar" then 1 else err)
+    ];
+
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrnames.exp b/tvix/eval/src/tests/nix_tests/eval-okay-attrnames.exp
new file mode 100644
index 000000000000..b4aa387e07b8
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrnames.exp
@@ -0,0 +1 @@
+"newxfoonewxy"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrnames.nix b/tvix/eval/src/tests/nix_tests/eval-okay-attrnames.nix
new file mode 100644
index 000000000000..e5b26e9f2e39
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrnames.nix
@@ -0,0 +1,11 @@
+with import ./lib.nix;
+
+let
+
+  attrs = {y = "y"; x = "x"; foo = "foo";} // rec {x = "newx"; bar = x;};
+
+  names = builtins.attrNames attrs;
+
+  values = map (name: builtins.getAttr name attrs) names;
+
+in assert values == builtins.attrValues attrs; concat values
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs.exp b/tvix/eval/src/tests/nix_tests/eval-okay-attrs.exp
new file mode 100644
index 000000000000..45b0f829eb33
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs.exp
@@ -0,0 +1 @@
+987
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs.nix b/tvix/eval/src/tests/nix_tests/eval-okay-attrs.nix
new file mode 100644
index 000000000000..810b31a5da96
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs.nix
@@ -0,0 +1,5 @@
+let {
+  as = { x = 123; y = 456; } // { z = 789; } // { z = 987; };
+
+  body = if as ? a then as.a else assert as ? z; as.z;
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs2.exp b/tvix/eval/src/tests/nix_tests/eval-okay-attrs2.exp
new file mode 100644
index 000000000000..45b0f829eb33
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs2.exp
@@ -0,0 +1 @@
+987
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs2.nix b/tvix/eval/src/tests/nix_tests/eval-okay-attrs2.nix
new file mode 100644
index 000000000000..9e06b83ac1fd
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs2.nix
@@ -0,0 +1,10 @@
+let {
+  as = { x = 123; y = 456; } // { z = 789; } // { z = 987; };
+
+  A = "a";
+  Z = "z";
+
+  body = if builtins.hasAttr A as
+         then builtins.getAttr A as
+         else assert builtins.hasAttr Z as; builtins.getAttr Z as;
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs3.exp b/tvix/eval/src/tests/nix_tests/eval-okay-attrs3.exp
new file mode 100644
index 000000000000..19de4fdf79f7
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs3.exp
@@ -0,0 +1 @@
+"foo 22 80 itchyxac"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs3.nix b/tvix/eval/src/tests/nix_tests/eval-okay-attrs3.nix
new file mode 100644
index 000000000000..f29de11fe660
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs3.nix
@@ -0,0 +1,22 @@
+let
+
+  config = 
+    {
+      services.sshd.enable = true;
+      services.sshd.port = 22;
+      services.httpd.port = 80;
+      hostName = "itchy";
+      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 = "x";
+      foo = {
+        a = "a";
+        b.c = "c";
+      };
+    };
+
+in
+  if config.services.sshd.enable
+  then "foo ${toString config.services.sshd.port} ${toString config.services.httpd.port} ${config.hostName}"
+       + "${config.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}"
+       + "${config.foo.a}"
+       + "${config.foo.b.c}"
+  else "bar"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs4.exp b/tvix/eval/src/tests/nix_tests/eval-okay-attrs4.exp
new file mode 100644
index 000000000000..1851731442d3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs4.exp
@@ -0,0 +1 @@
+[ true false true false false true false false ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs4.nix b/tvix/eval/src/tests/nix_tests/eval-okay-attrs4.nix
new file mode 100644
index 000000000000..43ec81210f38
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs4.nix
@@ -0,0 +1,7 @@
+let
+
+  as = { x.y.z = 123; a.b.c = 456; };
+
+  bs = null;
+
+in [ (as ? x) (as ? y) (as ? x.y.z) (as ? x.y.z.a) (as ? x.y.a) (as ? a.b.c) (bs ? x) (bs ? x.y.z) ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs5.exp b/tvix/eval/src/tests/nix_tests/eval-okay-attrs5.exp
new file mode 100644
index 000000000000..ce0430d78081
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs5.exp
@@ -0,0 +1 @@
+[ 123 "foo" 456 456 "foo" "xyzzy" "xyzzy" true ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-attrs5.nix b/tvix/eval/src/tests/nix_tests/eval-okay-attrs5.nix
new file mode 100644
index 000000000000..a4584cd3b398
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-attrs5.nix
@@ -0,0 +1,21 @@
+with import ./lib.nix;
+
+let
+
+  as = { x.y.z = 123; a.b.c = 456; };
+
+  bs = { f-o-o.bar = "foo"; };
+
+  or = x: y: x || y;
+  
+in
+  [ as.x.y.z
+    as.foo or "foo"
+    as.x.y.bla or as.a.b.c
+    as.a.b.c or as.x.y.z
+    as.x.y.bla or bs.f-o-o.bar or "xyzzy"
+    as.x.y.bla or bs.bar.foo or "xyzzy"
+    (123).bla or null.foo or "xyzzy"
+    # Backwards compatibility test.
+    (fold or [] [true false false])
+  ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.exp b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.exp
new file mode 100644
index 000000000000..3e754364cc9c
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.exp
@@ -0,0 +1 @@
+"a\nb"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.nix b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.nix
new file mode 100644
index 000000000000..7fef3dddd4dd
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-1.nix
@@ -0,0 +1,2 @@
+"a\
+b"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.exp b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.exp
new file mode 100644
index 000000000000..3e754364cc9c
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.exp
@@ -0,0 +1 @@
+"a\nb"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.nix b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.nix
new file mode 100644
index 000000000000..35ddf495c63b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-backslash-newline-2.nix
@@ -0,0 +1,2 @@
+''a''\
+b''
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.exp b/tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.exp
new file mode 100644
index 000000000000..0350b518a7ec
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.exp
@@ -0,0 +1 @@
+[ 5 4 "int" "tt" "float" 4 ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.nix b/tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.nix
new file mode 100644
index 000000000000..c841816222a5
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-builtins-add.nix
@@ -0,0 +1,8 @@
+[
+(builtins.add 2 3)
+(builtins.add 2 2)
+(builtins.typeOf (builtins.add 2  2))
+("t" + "t")
+(builtins.typeOf (builtins.add 2.0 2))
+(builtins.add 2.0 2)
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-builtins.exp b/tvix/eval/src/tests/nix_tests/eval-okay-builtins.exp
new file mode 100644
index 000000000000..0661686d611d
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-builtins.exp
@@ -0,0 +1 @@
+/foo
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-builtins.nix b/tvix/eval/src/tests/nix_tests/eval-okay-builtins.nix
new file mode 100644
index 000000000000..e9d65e88a817
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-builtins.nix
@@ -0,0 +1,12 @@
+assert builtins ? currentSystem;
+assert !builtins ? __currentSystem;
+
+let {
+
+  x = if builtins ? dirOf then builtins.dirOf /foo/bar else "";
+
+  y = if builtins ? fnord then builtins.fnord "foo" else "";
+
+  body = x + y;
+  
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.exp b/tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.nix b/tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.nix
new file mode 100644
index 000000000000..310a030df004
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-callable-attrs.nix
@@ -0,0 +1 @@
+({ __functor = self: x: self.foo && x; foo = false; } // { foo = true; }) true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-catattrs.exp b/tvix/eval/src/tests/nix_tests/eval-okay-catattrs.exp
new file mode 100644
index 000000000000..b4a1e66d6b8a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-catattrs.exp
@@ -0,0 +1 @@
+[ 1 2 ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-catattrs.nix b/tvix/eval/src/tests/nix_tests/eval-okay-catattrs.nix
new file mode 100644
index 000000000000..2c3dc10da528
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-catattrs.nix
@@ -0,0 +1 @@
+builtins.catAttrs "a" [ { a = 1; } { b = 0; } { a = 2; } ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-closure.exp b/tvix/eval/src/tests/nix_tests/eval-okay-closure.exp
new file mode 100644
index 000000000000..e7dbf978160d
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-closure.exp
@@ -0,0 +1 @@
+[ { foo = true; key = -13; } { foo = true; key = -12; } { foo = true; key = -11; } { foo = true; key = -9; } { foo = true; key = -8; } { foo = true; key = -7; } { foo = true; key = -5; } { foo = true; key = -4; } { foo = true; key = -3; } { key = -1; } { foo = true; key = 0; } { foo = true; key = 1; } { foo = true; key = 2; } { foo = true; key = 4; } { foo = true; key = 5; } { foo = true; key = 6; } { key = 8; } { foo = true; key = 9; } { foo = true; key = 10; } { foo = true; key = 13; } { foo = true; key = 14; } { foo = true; key = 15; } { key = 17; } { foo = true; key = 18; } { foo = true; key = 19; } { foo = true; key = 22; } { foo = true; key = 23; } { key = 26; } { foo = true; key = 27; } { foo = true; key = 28; } { foo = true; key = 31; } { foo = true; key = 32; } { key = 35; } { foo = true; key = 36; } { foo = true; key = 40; } { foo = true; key = 41; } { key = 44; } { foo = true; key = 45; } { foo = true; key = 49; } { key = 53; } { foo = true; key = 54; } { foo = true; key = 58; } { key = 62; } { foo = true; key = 67; } { key = 71; } { key = 80; } ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-closure.nix b/tvix/eval/src/tests/nix_tests/eval-okay-closure.nix
new file mode 100644
index 000000000000..cccd4dc35730
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-closure.nix
@@ -0,0 +1,13 @@
+let
+
+  closure = builtins.genericClosure {
+    startSet = [{key = 80;}];
+    operator = {key, foo ? false}:
+      if builtins.lessThan key 0
+      then []
+      else [{key = builtins.sub key 9;} {key = builtins.sub key 13; foo = true;}];
+  };
+
+  sort = (import ./lib.nix).sortBy (a: b: builtins.lessThan a.key b.key);
+
+in sort closure
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-comments.exp b/tvix/eval/src/tests/nix_tests/eval-okay-comments.exp
new file mode 100644
index 000000000000..7182dc2f9b8e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-comments.exp
@@ -0,0 +1 @@
+"abcdefghijklmnopqrstuvwxyz"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-comments.nix b/tvix/eval/src/tests/nix_tests/eval-okay-comments.nix
new file mode 100644
index 000000000000..cb2cce218029
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-comments.nix
@@ -0,0 +1,59 @@
+# A simple comment
+"a"+ # And another
+## A double comment
+"b"+  ## And another
+# Nested # comments #
+"c"+   # and # some # other #
+# An empty line, following here:
+
+"d"+      # and a comment not starting the line !
+
+"e"+
+/* multiline comments */
+"f" +
+/* multiline
+   comments,
+   on
+   multiple
+   lines
+*/
+"g" +
+# Small, tricky comments
+/**/ "h"+ /*/*/ "i"+ /***/ "j"+ /* /*/ "k"+ /*/* /*/ "l"+
+# Comments with an even number of ending '*' used to fail:
+"m"+
+/* */ /* **/ /* ***/ /* ****/ "n"+
+/* */ /** */ /*** */ /**** */ "o"+
+/** **/ /*** ***/ /**** ****/ "p"+
+/* * ** *** **** ***** */     "q"+
+# Random comments
+/* ***** ////// * / * / /* */ "r"+
+# Mixed comments
+/* # */
+"s"+
+# /* #
+"t"+
+# /* # */
+"u"+
+# /*********/
+"v"+
+## */*
+"w"+
+/*
+ * Multiline, decorated comments
+ * # This ain't a nest'd comm'nt
+ */
+"x"+
+''${/** with **/"y"
+  # real
+  /* comments
+     inside ! # */
+
+  # (and empty lines)
+
+}''+          /* And a multiline comment,
+                 on the same line,
+                 after some spaces
+*/             # followed by a one-line comment
+"z"
+/* EOF */
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-concat.exp b/tvix/eval/src/tests/nix_tests/eval-okay-concat.exp
new file mode 100644
index 000000000000..bb4bbd577410
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-concat.exp
@@ -0,0 +1 @@
+[ 1 2 3 4 5 6 7 8 9 ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-concat.nix b/tvix/eval/src/tests/nix_tests/eval-okay-concat.nix
new file mode 100644
index 000000000000..d158a9bf05b9
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-concat.nix
@@ -0,0 +1 @@
+[1 2 3] ++ [4 5 6] ++ [7 8 9]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-concatmap.exp b/tvix/eval/src/tests/nix_tests/eval-okay-concatmap.exp
new file mode 100644
index 000000000000..3b8be7739deb
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-concatmap.exp
@@ -0,0 +1 @@
+[ [ 1 3 5 7 9 ] [ "a" "z" "b" "z" ] ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-concatmap.nix b/tvix/eval/src/tests/nix_tests/eval-okay-concatmap.nix
new file mode 100644
index 000000000000..97da5d37a412
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-concatmap.nix
@@ -0,0 +1,5 @@
+with import ./lib.nix;
+
+[ (builtins.concatMap (x: if x / 2 * 2 == x then [] else [ x ]) (range 0 10))
+  (builtins.concatMap (x: [x] ++ ["z"]) ["a" "b"])
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.exp b/tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.exp
new file mode 100644
index 000000000000..93987647ffe6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.exp
@@ -0,0 +1 @@
+[ "" "foobarxyzzy" "foo, bar, xyzzy" "foo" "" ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.nix b/tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.nix
new file mode 100644
index 000000000000..adc4c41bd551
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-concatstringssep.nix
@@ -0,0 +1,8 @@
+with builtins;
+
+[ (concatStringsSep "" [])
+  (concatStringsSep "" ["foo" "bar" "xyzzy"])
+  (concatStringsSep ", " ["foo" "bar" "xyzzy"])
+  (concatStringsSep ", " ["foo"])
+  (concatStringsSep ", " [])
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-deepseq.exp b/tvix/eval/src/tests/nix_tests/eval-okay-deepseq.exp
new file mode 100644
index 000000000000..8d38505c1686
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-deepseq.exp
@@ -0,0 +1 @@
+456
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-deepseq.nix b/tvix/eval/src/tests/nix_tests/eval-okay-deepseq.nix
new file mode 100644
index 000000000000..53aa4b1dc251
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-deepseq.nix
@@ -0,0 +1 @@
+builtins.deepSeq (let as = { x = 123; y = as; }; in as) 456
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.exp b/tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.exp
new file mode 100644
index 000000000000..eaacb55c1aff
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.exp
@@ -0,0 +1 @@
+"b-overridden"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.nix b/tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.nix
new file mode 100644
index 000000000000..84b388c27130
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-delayed-with-inherit.nix
@@ -0,0 +1,24 @@
+let
+  pkgs_ = with pkgs; {
+    a = derivation {
+      name = "a";
+      system = builtins.currentSystem;
+      builder = "/bin/sh";
+      args = [ "-c" "touch $out" ];
+      inherit b;
+    };
+
+    inherit b;
+  };
+
+  packageOverrides = p: {
+    b = derivation {
+      name = "b-overridden";
+      system = builtins.currentSystem;
+      builder = "/bin/sh";
+      args = [ "-c" "touch $out" ];
+    };
+  };
+
+  pkgs = pkgs_ // (packageOverrides pkgs_);
+in pkgs.a.b.name
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.exp b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.nix b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.nix
new file mode 100644
index 000000000000..6d57bf854908
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-2.nix
@@ -0,0 +1 @@
+{ a."${"b"}" = true; a."${"c"}" = false; }.a.b
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.exp b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.exp
new file mode 100644
index 000000000000..df8750afc036
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.exp
@@ -0,0 +1 @@
+{ binds = true; hasAttrs = true; multiAttrs = true; recBinds = true; selectAttrs = true; selectOrAttrs = true; }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.nix b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.nix
new file mode 100644
index 000000000000..0dbe15e6384c
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs-bare.nix
@@ -0,0 +1,17 @@
+let
+  aString = "a";
+
+  bString = "b";
+in {
+  hasAttrs = { a.b = null; } ? ${aString}.b;
+
+  selectAttrs = { a.b = true; }.a.${bString};
+
+  selectOrAttrs = { }.${aString} or true;
+
+  binds = { ${aString}."${bString}c" = true; }.a.bc;
+
+  recBinds = rec { ${bString} = a; a = true; }.b;
+
+  multiAttrs = { ${aString} = true; ${bString} = false; }.a;
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.exp b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.exp
new file mode 100644
index 000000000000..df8750afc036
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.exp
@@ -0,0 +1 @@
+{ binds = true; hasAttrs = true; multiAttrs = true; recBinds = true; selectAttrs = true; selectOrAttrs = true; }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.nix b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.nix
new file mode 100644
index 000000000000..ee02ac7e6579
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-dynamic-attrs.nix
@@ -0,0 +1,17 @@
+let
+  aString = "a";
+
+  bString = "b";
+in {
+  hasAttrs = { a.b = null; } ? "${aString}".b;
+
+  selectAttrs = { a.b = true; }.a."${bString}";
+
+  selectOrAttrs = { }."${aString}" or true;
+
+  binds = { "${aString}"."${bString}c" = true; }.a.bc;
+
+  recBinds = rec { "${bString}" = a; a = true; }.b;
+
+  multiAttrs = { "${aString}" = true; "${bString}" = false; }.a;
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-elem.exp b/tvix/eval/src/tests/nix_tests/eval-okay-elem.exp
new file mode 100644
index 000000000000..3cf6c0e962f0
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-elem.exp
@@ -0,0 +1 @@
+[ true false 30 ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-elem.nix b/tvix/eval/src/tests/nix_tests/eval-okay-elem.nix
new file mode 100644
index 000000000000..71ea7a4ed03d
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-elem.nix
@@ -0,0 +1,6 @@
+with import ./lib.nix;
+
+let xs = range 10 40; in
+
+[ (builtins.elem 23 xs) (builtins.elem 42 xs) (builtins.elemAt xs 20) ]
+
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-empty-args.exp b/tvix/eval/src/tests/nix_tests/eval-okay-empty-args.exp
new file mode 100644
index 000000000000..cb5537d5d7ce
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-empty-args.exp
@@ -0,0 +1 @@
+"ab"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-empty-args.nix b/tvix/eval/src/tests/nix_tests/eval-okay-empty-args.nix
new file mode 100644
index 000000000000..78c133afdd94
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-empty-args.nix
@@ -0,0 +1 @@
+({}: {x,y,}: "${x}${y}") {} {x = "a"; y = "b";}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-eq.exp b/tvix/eval/src/tests/nix_tests/eval-okay-eq.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-eq.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-eq.nix b/tvix/eval/src/tests/nix_tests/eval-okay-eq.nix
new file mode 100644
index 000000000000..73d200b38141
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-eq.nix
@@ -0,0 +1,3 @@
+["foobar" (rec {x = 1; y = x;})]
+==
+[("foo" + "bar") ({x = 1; y = 1;})]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-filter.exp b/tvix/eval/src/tests/nix_tests/eval-okay-filter.exp
new file mode 100644
index 000000000000..355d51c27d8f
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-filter.exp
@@ -0,0 +1 @@
+[ 0 2 4 6 8 10 100 102 104 106 108 110 ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-filter.nix b/tvix/eval/src/tests/nix_tests/eval-okay-filter.nix
new file mode 100644
index 000000000000..85109b0d0eb8
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-filter.nix
@@ -0,0 +1,5 @@
+with import ./lib.nix;
+
+builtins.filter
+  (x: x / 2 * 2 == x)
+  (builtins.concatLists [ (range 0 10) (range 100 110) ])
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-flatten.exp b/tvix/eval/src/tests/nix_tests/eval-okay-flatten.exp
new file mode 100644
index 000000000000..b979b2b8b9bc
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-flatten.exp
@@ -0,0 +1 @@
+"1234567"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-flatten.nix b/tvix/eval/src/tests/nix_tests/eval-okay-flatten.nix
new file mode 100644
index 000000000000..fe911e9683e2
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-flatten.nix
@@ -0,0 +1,8 @@
+with import ./lib.nix;
+
+let {
+
+  l = ["1" "2" ["3" ["4"] ["5" "6"]] "7"];
+
+  body = concat (flatten l);
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-float.exp b/tvix/eval/src/tests/nix_tests/eval-okay-float.exp
new file mode 100644
index 000000000000..3c50a8adce86
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-float.exp
@@ -0,0 +1 @@
+[ 3.4 3.5 2.5 1.5 ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-float.nix b/tvix/eval/src/tests/nix_tests/eval-okay-float.nix
new file mode 100644
index 000000000000..b2702c7b1668
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-float.nix
@@ -0,0 +1,6 @@
+[
+  (1.1 + 2.3)
+  (builtins.add (0.5 + 0.5) (2.0 + 0.5))
+  ((0.5 + 0.5) * (2.0 + 0.5))
+  ((1.5 + 1.5) / (0.5 * 4.0))
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.exp b/tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.exp
new file mode 100644
index 000000000000..837e12b406f6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.exp
@@ -0,0 +1 @@
+500500
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.nix b/tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.nix
new file mode 100644
index 000000000000..3b87188d243b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-foldlStrict.nix
@@ -0,0 +1,3 @@
+with import ./lib.nix;
+
+builtins.foldl' (x: y: x + y) 0 (range 1 1000)
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.exp b/tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.exp
new file mode 100644
index 000000000000..d0dd3af2c814
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.exp
@@ -0,0 +1 @@
+[ { clients = { data = [ [ "gamma" "delta" ] [ 1 2 ] ]; hosts = [ "alpha" "omega" ]; }; database = { connection_max = 5000; enabled = true; ports = [ 8001 8001 8002 ]; server = "192.168.1.1"; }; owner = { name = "Tom Preston-Werner"; }; servers = { alpha = { dc = "eqdc10"; ip = "10.0.0.1"; }; beta = { dc = "eqdc10"; ip = "10.0.0.2"; }; }; title = "TOML Example"; } { "1234" = "value"; "127.0.0.1" = "value"; a = { b = { c = { }; }; }; arr1 = [ 1 2 3 ]; arr2 = [ "red" "yellow" "green" ]; arr3 = [ [ 1 2 ] [ 3 4 5 ] ]; arr4 = [ "all" "strings" "are the same" "type" ]; arr5 = [ [ 1 2 ] [ "a" "b" "c" ] ]; arr7 = [ 1 2 3 ]; arr8 = [ 1 2 ]; bare-key = "value"; bare_key = "value"; bin1 = 214; bool1 = true; bool2 = false; "character encoding" = "value"; d = { e = { f = { }; }; }; dog = { "tater.man" = { type = { name = "pug"; }; }; }; flt1 = 1; flt2 = 3.1415; flt3 = -0.01; flt4 = 5e+22; flt5 = 1e+06; flt6 = -0.02; flt7 = 6.626e-34; flt8 = 9.22462e+06; fruit = [ { name = "apple"; physical = { color = "red"; shape = "round"; }; variety = [ { name = "red delicious"; } { name = "granny smith"; } ]; } { name = "banana"; variety = [ { name = "plantain"; } ]; } ]; g = { h = { i = { }; }; }; hex1 = 3735928559; hex2 = 3735928559; hex3 = 3735928559; int1 = 99; int2 = 42; int3 = 0; int4 = -17; int5 = 1000; int6 = 5349221; int7 = 12345; j = { "ʞ" = { l = { }; }; }; key = "value"; key2 = "value"; name = "Orange"; oct1 = 342391; oct2 = 493; physical = { color = "orange"; shape = "round"; }; products = [ { name = "Hammer"; sku = 738594937; } { } { color = "gray"; name = "Nail"; sku = 284758393; } ]; "quoted \"value\"" = "value"; site = { "google.com" = true; }; str = "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."; table-1 = { key1 = "some string"; key2 = 123; }; table-2 = { key1 = "another string"; key2 = 456; }; x = { y = { z = { w = { animal = { type = { name = "pug"; }; }; name = { first = "Tom"; last = "Preston-Werner"; }; point = { x = 1; y = 2; }; }; }; }; }; "ʎǝʞ" = "value"; } { metadata = { "checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"; "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"; "checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"; "checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef"; }; package = [ { dependencies = [ "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" ]; name = "aho-corasick"; source = "registry+https://github.com/rust-lang/crates.io-index"; version = "0.6.4"; } { name = "ansi_term"; source = "registry+https://github.com/rust-lang/crates.io-index"; version = "0.9.0"; } { dependencies = [ "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)" "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" ]; name = "atty"; source = "registry+https://github.com/rust-lang/crates.io-index"; version = "0.2.10"; } ]; } { a = [ [ { b = true; } ] ]; c = [ [ { d = true; } ] ]; e = [ [ 123 ] ]; } ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.nix b/tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.nix
new file mode 100644
index 000000000000..963932689942
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-fromTOML.nix
@@ -0,0 +1,208 @@
+[
+
+  (builtins.fromTOML ''
+    # This is a TOML document.
+
+    title = "TOML Example"
+
+    [owner]
+    name = "Tom Preston-Werner"
+    #dob = 1979-05-27T07:32:00-08:00 # First class dates
+
+    [database]
+    server = "192.168.1.1"
+    ports = [ 8001, 8001, 8002 ]
+    connection_max = 5000
+    enabled = true
+
+    [servers]
+
+      # Indentation (tabs and/or spaces) is allowed but not required
+      [servers.alpha]
+      ip = "10.0.0.1"
+      dc = "eqdc10"
+
+      [servers.beta]
+      ip = "10.0.0.2"
+      dc = "eqdc10"
+
+    [clients]
+    data = [ ["gamma", "delta"], [1, 2] ]
+
+    # Line breaks are OK when inside arrays
+    hosts = [
+      "alpha",
+      "omega"
+    ]
+  '')
+
+  (builtins.fromTOML ''
+    key = "value"
+    bare_key = "value"
+    bare-key = "value"
+    1234 = "value"
+
+    "127.0.0.1" = "value"
+    "character encoding" = "value"
+    "ʎǝʞ" = "value"
+    'key2' = "value"
+    'quoted "value"' = "value"
+
+    name = "Orange"
+
+    physical.color = "orange"
+    physical.shape = "round"
+    site."google.com" = true
+
+    # This is legal according to the spec, but cpptoml doesn't handle it.
+    #a.b.c = 1
+    #a.d = 2
+
+    str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
+
+    int1 = +99
+    int2 = 42
+    int3 = 0
+    int4 = -17
+    int5 = 1_000
+    int6 = 5_349_221
+    int7 = 1_2_3_4_5
+
+    hex1 = 0xDEADBEEF
+    hex2 = 0xdeadbeef
+    hex3 = 0xdead_beef
+
+    oct1 = 0o01234567
+    oct2 = 0o755
+
+    bin1 = 0b11010110
+
+    flt1 = +1.0
+    flt2 = 3.1415
+    flt3 = -0.01
+    flt4 = 5e+22
+    flt5 = 1e6
+    flt6 = -2E-2
+    flt7 = 6.626e-34
+    flt8 = 9_224_617.445_991_228_313
+
+    bool1 = true
+    bool2 = false
+
+    # FIXME: not supported because Nix doesn't have a date/time type.
+    #odt1 = 1979-05-27T07:32:00Z
+    #odt2 = 1979-05-27T00:32:00-07:00
+    #odt3 = 1979-05-27T00:32:00.999999-07:00
+    #odt4 = 1979-05-27 07:32:00Z
+    #ldt1 = 1979-05-27T07:32:00
+    #ldt2 = 1979-05-27T00:32:00.999999
+    #ld1 = 1979-05-27
+    #lt1 = 07:32:00
+    #lt2 = 00:32:00.999999
+
+    arr1 = [ 1, 2, 3 ]
+    arr2 = [ "red", "yellow", "green" ]
+    arr3 = [ [ 1, 2 ], [3, 4, 5] ]
+    arr4 = [ "all", 'strings', """are the same""", ''''type'''']
+    arr5 = [ [ 1, 2 ], ["a", "b", "c"] ]
+
+    arr7 = [
+      1, 2, 3
+    ]
+
+    arr8 = [
+      1,
+      2, # this is ok
+    ]
+
+    [table-1]
+    key1 = "some string"
+    key2 = 123
+
+
+    [table-2]
+    key1 = "another string"
+    key2 = 456
+
+    [dog."tater.man"]
+    type.name = "pug"
+
+    [a.b.c]
+    [ d.e.f ]
+    [ g .  h  . i ]
+    [ j . "ʞ" . 'l' ]
+    [x.y.z.w]
+
+    name = { first = "Tom", last = "Preston-Werner" }
+    point = { x = 1, y = 2 }
+    animal = { type.name = "pug" }
+
+    [[products]]
+    name = "Hammer"
+    sku = 738594937
+
+    [[products]]
+
+    [[products]]
+    name = "Nail"
+    sku = 284758393
+    color = "gray"
+
+    [[fruit]]
+      name = "apple"
+
+      [fruit.physical]
+        color = "red"
+        shape = "round"
+
+      [[fruit.variety]]
+        name = "red delicious"
+
+      [[fruit.variety]]
+        name = "granny smith"
+
+    [[fruit]]
+      name = "banana"
+
+      [[fruit.variety]]
+        name = "plantain"
+  '')
+
+  (builtins.fromTOML ''
+    [[package]]
+    name = "aho-corasick"
+    version = "0.6.4"
+    source = "registry+https://github.com/rust-lang/crates.io-index"
+    dependencies = [
+     "memchr 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+    ]
+
+    [[package]]
+    name = "ansi_term"
+    version = "0.9.0"
+    source = "registry+https://github.com/rust-lang/crates.io-index"
+
+    [[package]]
+    name = "atty"
+    version = "0.2.10"
+    source = "registry+https://github.com/rust-lang/crates.io-index"
+    dependencies = [
+     "libc 0.2.42 (registry+https://github.com/rust-lang/crates.io-index)",
+     "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+     "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+    ]
+
+    [metadata]
+    "checksum aho-corasick 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4"
+    "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+    "checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
+    "checksum arrayvec 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "a1e964f9e24d588183fcb43503abda40d288c8657dfc27311516ce2f05675aef"
+  '')
+
+  (builtins.fromTOML ''
+    a = [[{ b = true }]]
+    c = [ [ { d = true } ] ]
+    e = [[123]]
+  '')
+
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.exp b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.exp
new file mode 100644
index 000000000000..add5505a8287
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.exp
@@ -0,0 +1 @@
+"quote \" reverse solidus \\ solidus / backspace  formfeed  newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace  1 char unicode encoded e with accent é 2 char unicode encoded s with caron š 3 char unicode encoded rightwards arrow →"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.nix b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.nix
new file mode 100644
index 000000000000..f00713507732
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson-escapes.nix
@@ -0,0 +1,3 @@
+# This string contains all supported escapes in a JSON string, per json.org
+# \b and \f are not supported by Nix
+builtins.fromJSON ''"quote \" reverse solidus \\ solidus \/ backspace \b formfeed \f newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace \u0008 1 char unicode encoded e with accent \u00e9 2 char unicode encoded s with caron \u0161 3 char unicode encoded rightwards arrow \u2192"''
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-fromjson.exp b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-fromjson.nix b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson.nix
new file mode 100644
index 000000000000..e1c0f86cc4e4
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-fromjson.nix
@@ -0,0 +1,35 @@
+builtins.fromJSON
+  ''
+    {
+      "Video": {
+          "Title":  "The Penguin Chronicles",
+          "Width":  1920,
+          "Height": 1080,
+          "EmbeddedData": [3.14159, 23493,null, true  ,false, -10],
+          "Thumb": {
+              "Url":    "http://www.example.com/video/5678931",
+              "Width":  200,
+              "Height": 250
+          },
+          "Subtitle" : false,
+          "Latitude":  46.2051,
+          "Longitude": 6.0723
+        }
+    }
+  ''
+==
+  { Video =
+    { Title = "The Penguin Chronicles";
+      Width = 1920;
+      Height = 1080;
+      EmbeddedData = [ 3.14159 23493 null true false (0-10) ];
+      Thumb =
+        { Url = "http://www.example.com/video/5678931";
+          Width = 200;
+          Height = 250;
+        };
+      Subtitle = false;
+      Latitude = 46.2051;
+      Longitude = 6.0723;
+    };
+  }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-hash.exp b/tvix/eval/src/tests/nix_tests/eval-okay-hash.exp
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-hash.exp
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-if.exp b/tvix/eval/src/tests/nix_tests/eval-okay-if.exp
new file mode 100644
index 000000000000..00750edc07d6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-if.exp
@@ -0,0 +1 @@
+3
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-if.nix b/tvix/eval/src/tests/nix_tests/eval-okay-if.nix
new file mode 100644
index 000000000000..23e4c74d5016
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-if.nix
@@ -0,0 +1 @@
+if "foo" != "f" + "oo" then 1 else if false then 2 else 3
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-ind-string.exp b/tvix/eval/src/tests/nix_tests/eval-okay-ind-string.exp
new file mode 100644
index 000000000000..7862331fa551
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-ind-string.exp
@@ -0,0 +1 @@
+"This is an indented multi-line string\nliteral.  An amount of whitespace at\nthe start of each line matching the minimum\nindentation of all lines in the string\nliteral together will be removed.  Thus,\nin this case four spaces will be\nstripped from each line, even though\n  THIS LINE is indented six spaces.\n\nAlso, empty lines don't count in the\ndetermination of the indentation level (the\nprevious empty line has indentation 0, but\nit doesn't matter).\nIf the string starts with whitespace\n  followed by a newline, it's stripped, but\n  that's not the case here. Two spaces are\n  stripped because of the \"  \" at the start. \nThis line is indented\na bit further.\nAnti-quotations, like so, are\nalso allowed.\n  The \\ is not special here.\n' can be followed by any character except another ', e.g. 'x'.\nLikewise for $, e.g. $$ or $varName.\nBut ' followed by ' is special, as is $ followed by {.\nIf you want them, use anti-quotations: '', \${.\n   Tabs are not interpreted as whitespace (since we can't guess\n   what tab settings are intended), so don't use them.\n\tThis line starts with a space and a tab, so only one\n   space will be stripped from each line.\nAlso note that if the last line (just before the closing ' ')\nconsists only of whitespace, it's ignored.  But here there is\nsome non-whitespace stuff, so the line isn't removed. \nThis shows a hacky way to preserve an empty line after the start.\nBut there's no reason to do so: you could just repeat the empty\nline.\n  Similarly you can force an indentation level,\n  in this case to 2 spaces.  This works because the anti-quote\n  is significant (not whitespace).\nstart on network-interfaces\n\nstart script\n\n  rm -f /var/run/opengl-driver\n  ln -sf 123 /var/run/opengl-driver\n\n  rm -f /var/log/slim.log\n   \nend script\n\nenv SLIM_CFGFILE=abc\nenv SLIM_THEMESDIR=def\nenv FONTCONFIG_FILE=/etc/fonts/fonts.conf  \t\t\t\t# !!! cleanup\nenv XKB_BINDIR=foo/bin         \t\t\t\t# Needed for the Xkb extension.\nenv LD_LIBRARY_PATH=libX11/lib:libXext/lib:/usr/lib/          # related to xorg-sys-opengl - needed to load libglx for (AI)GLX support (for compiz)\n\nenv XORG_DRI_DRIVER_PATH=nvidiaDrivers/X11R6/lib/modules/drivers/ \n\nexec slim/bin/slim\nEscaping of ' followed by ': ''\nEscaping of $ followed by {: \${\nAnd finally to interpret \\n etc. as in a string: \n, \r, \t.\nfoo\n'bla'\nbar\ncut -d $'\\t' -f 1\nending dollar $$\n"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-ind-string.nix b/tvix/eval/src/tests/nix_tests/eval-okay-ind-string.nix
new file mode 100644
index 000000000000..1669dc0648ea
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-ind-string.nix
@@ -0,0 +1,128 @@
+let
+
+  s1 = ''
+    This is an indented multi-line string
+    literal.  An amount of whitespace at
+    the start of each line matching the minimum
+    indentation of all lines in the string
+    literal together will be removed.  Thus,
+    in this case four spaces will be
+    stripped from each line, even though
+      THIS LINE is indented six spaces.
+
+    Also, empty lines don't count in the
+    determination of the indentation level (the
+    previous empty line has indentation 0, but
+    it doesn't matter).
+  '';
+
+  s2 = ''  If the string starts with whitespace
+    followed by a newline, it's stripped, but
+    that's not the case here. Two spaces are
+    stripped because of the "  " at the start. 
+  '';
+
+  s3 = ''
+      This line is indented
+      a bit further.
+        ''; # indentation of last line doesn't count if it's empty
+
+  s4 = ''
+    Anti-quotations, like ${if true then "so" else "not so"}, are
+    also allowed.
+  '';
+
+  s5 = ''
+      The \ is not special here.
+    ' can be followed by any character except another ', e.g. 'x'.
+    Likewise for $, e.g. $$ or $varName.
+    But ' followed by ' is special, as is $ followed by {.
+    If you want them, use anti-quotations: ${"''"}, ${"\${"}.
+  '';
+
+  s6 = ''  
+    Tabs are not interpreted as whitespace (since we can't guess
+    what tab settings are intended), so don't use them.
+ 	This line starts with a space and a tab, so only one
+    space will be stripped from each line.
+  '';
+
+  s7 = ''
+    Also note that if the last line (just before the closing ' ')
+    consists only of whitespace, it's ignored.  But here there is
+    some non-whitespace stuff, so the line isn't removed. '';
+
+  s8 = ''    ${""}
+    This shows a hacky way to preserve an empty line after the start.
+    But there's no reason to do so: you could just repeat the empty
+    line.
+  '';
+
+  s9 = ''
+  ${""}  Similarly you can force an indentation level,
+    in this case to 2 spaces.  This works because the anti-quote
+    is significant (not whitespace).
+  '';
+
+  s10 = ''
+  '';
+
+  s11 = '''';
+
+  s12 = ''   '';
+
+  s13 = ''
+    start on network-interfaces
+
+    start script
+    
+      rm -f /var/run/opengl-driver
+      ${if true
+        then "ln -sf 123 /var/run/opengl-driver"
+        else if true
+        then "ln -sf 456 /var/run/opengl-driver"
+        else ""
+      }
+
+      rm -f /var/log/slim.log
+       
+    end script
+
+    env SLIM_CFGFILE=${"abc"}
+    env SLIM_THEMESDIR=${"def"}
+    env FONTCONFIG_FILE=/etc/fonts/fonts.conf  				# !!! cleanup
+    env XKB_BINDIR=${"foo"}/bin         				# Needed for the Xkb extension.
+    env LD_LIBRARY_PATH=${"libX11"}/lib:${"libXext"}/lib:/usr/lib/          # related to xorg-sys-opengl - needed to load libglx for (AI)GLX support (for compiz)
+
+    ${if true
+      then "env XORG_DRI_DRIVER_PATH=${"nvidiaDrivers"}/X11R6/lib/modules/drivers/"
+    else if true
+      then "env XORG_DRI_DRIVER_PATH=${"mesa"}/lib/modules/dri"
+      else ""
+    } 
+
+    exec ${"slim"}/bin/slim
+  '';
+
+  s14 = ''
+    Escaping of ' followed by ': '''
+    Escaping of $ followed by {: ''${
+    And finally to interpret \n etc. as in a string: ''\n, ''\r, ''\t.
+  '';
+
+  # Regression test: antiquotation in '${x}' should work, but didn't.
+  s15 = let x = "bla"; in ''
+    foo
+    '${x}'
+    bar
+  '';
+
+  # Regression test: accept $'.
+  s16 = ''
+    cut -d $'\t' -f 1
+  '';
+
+  # Accept dollars at end of strings 
+  s17 = ''ending dollar $'' + ''$'' + "\n";
+
+in s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 + s12 + s13 + s14 + s15 + s16 + s17
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-let.exp b/tvix/eval/src/tests/nix_tests/eval-okay-let.exp
new file mode 100644
index 000000000000..14e24d419005
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-let.exp
@@ -0,0 +1 @@
+"foobar"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-let.nix b/tvix/eval/src/tests/nix_tests/eval-okay-let.nix
new file mode 100644
index 000000000000..fe118c5282e3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-let.nix
@@ -0,0 +1,5 @@
+let {
+  x = "foo";
+  y = "bar";
+  body = x + y;
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-list.exp b/tvix/eval/src/tests/nix_tests/eval-okay-list.exp
new file mode 100644
index 000000000000..f784f26d83f4
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-list.exp
@@ -0,0 +1 @@
+"foobarblatest"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-list.nix b/tvix/eval/src/tests/nix_tests/eval-okay-list.nix
new file mode 100644
index 000000000000..d433bcf908ba
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-list.nix
@@ -0,0 +1,7 @@
+with import ./lib.nix;
+
+let {
+
+  body = concat ["foo" "bar" "bla" "test"];
+    
+}
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.exp b/tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.exp
new file mode 100644
index 000000000000..74abef7bc6ed
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.exp
@@ -0,0 +1 @@
+"AAbar"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.nix b/tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.nix
new file mode 100644
index 000000000000..4186e029b538
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-listtoattrs.nix
@@ -0,0 +1,11 @@
+# this test shows how to use listToAttrs and that evaluation is still lazy (throw isn't called)
+with import ./lib.nix;
+
+let 
+  asi = name: value : { inherit name value; };
+  list = [ ( asi "a" "A" ) ( asi "b" "B" ) ];
+  a = builtins.listToAttrs list;
+  b = builtins.listToAttrs ( list ++ list );
+  r = builtins.listToAttrs [ (asi "result" [ a b ]) ( asi "throw" (throw "this should not be thrown")) ];
+  x = builtins.listToAttrs [ (asi "foo" "bar") (asi "foo" "bla") ];
+in concat (map (x: x.a) r.result) + x.foo
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-logic.exp b/tvix/eval/src/tests/nix_tests/eval-okay-logic.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-logic.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-logic.nix b/tvix/eval/src/tests/nix_tests/eval-okay-logic.nix
new file mode 100644
index 000000000000..fbb12794401f
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-logic.nix
@@ -0,0 +1 @@
+assert !false && (true || false) -> true; 1
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-map.exp b/tvix/eval/src/tests/nix_tests/eval-okay-map.exp
new file mode 100644
index 000000000000..dbb64f717b96
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-map.exp
@@ -0,0 +1 @@
+"foobarblabarxyzzybar"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-map.nix b/tvix/eval/src/tests/nix_tests/eval-okay-map.nix
new file mode 100644
index 000000000000..a76c1d811454
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-map.nix
@@ -0,0 +1,3 @@
+with import ./lib.nix;
+
+concat (map (x: x + "bar") [ "foo" "bla" "xyzzy" ])
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.exp b/tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.exp
new file mode 100644
index 000000000000..3f113f17bab1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.exp
@@ -0,0 +1 @@
+{ x = "x-foo"; y = "y-bar"; }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.nix b/tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.nix
new file mode 100644
index 000000000000..f075b6275e5a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-mapattrs.nix
@@ -0,0 +1,3 @@
+with import ./lib.nix;
+
+builtins.mapAttrs (name: value: name + "-" + value) { x = "foo"; y = "bar"; }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-nested-with.exp b/tvix/eval/src/tests/nix_tests/eval-okay-nested-with.exp
new file mode 100644
index 000000000000..0cfbf08886fc
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-nested-with.exp
@@ -0,0 +1 @@
+2
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-nested-with.nix b/tvix/eval/src/tests/nix_tests/eval-okay-nested-with.nix
new file mode 100644
index 000000000000..ba9d79aa79b1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-nested-with.nix
@@ -0,0 +1,3 @@
+with { x = 1; };
+with { x = 2; };
+x
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-new-let.exp b/tvix/eval/src/tests/nix_tests/eval-okay-new-let.exp
new file mode 100644
index 000000000000..f98b388071c2
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-new-let.exp
@@ -0,0 +1 @@
+"xyzzyfoobar"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-new-let.nix b/tvix/eval/src/tests/nix_tests/eval-okay-new-let.nix
new file mode 100644
index 000000000000..738123141508
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-new-let.nix
@@ -0,0 +1,14 @@
+let
+
+  f = z: 
+
+    let
+      x = "foo";
+      y = "bar";
+      body = 1; # compat test
+    in
+      z + x + y;
+
+  arg = "xyzzy";
+
+in f arg
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.exp b/tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.nix b/tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.nix
new file mode 100644
index 000000000000..b060c0bc9850
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-null-dynamic-attrs.nix
@@ -0,0 +1 @@
+{ ${null} = true; } == {}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-partition.exp b/tvix/eval/src/tests/nix_tests/eval-okay-partition.exp
new file mode 100644
index 000000000000..cd8b8b020c05
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-partition.exp
@@ -0,0 +1 @@
+{ right = [ 0 2 4 6 8 10 100 102 104 106 108 110 ]; wrong = [ 1 3 5 7 9 101 103 105 107 109 ]; }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-partition.nix b/tvix/eval/src/tests/nix_tests/eval-okay-partition.nix
new file mode 100644
index 000000000000..846d2ce49486
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-partition.nix
@@ -0,0 +1,5 @@
+with import ./lib.nix;
+
+builtins.partition
+  (x: x / 2 * 2 == x)
+  (builtins.concatLists [ (range 0 10) (range 100 110) ])
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-pathexists.exp b/tvix/eval/src/tests/nix_tests/eval-okay-pathexists.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-pathexists.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-pathexists.nix b/tvix/eval/src/tests/nix_tests/eval-okay-pathexists.nix
new file mode 100644
index 000000000000..50c28ee0cd30
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-pathexists.nix
@@ -0,0 +1,5 @@
+builtins.pathExists (builtins.toPath ./lib.nix)
+&& builtins.pathExists (builtins.toPath (builtins.toString ./lib.nix))
+&& !builtins.pathExists (builtins.toPath (builtins.toString ./bla.nix))
+&& builtins.pathExists ./lib.nix
+&& !builtins.pathExists ./bla.nix
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-patterns.exp b/tvix/eval/src/tests/nix_tests/eval-okay-patterns.exp
new file mode 100644
index 000000000000..a4304010fe80
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-patterns.exp
@@ -0,0 +1 @@
+"abcxyzDDDDEFijk"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-patterns.nix b/tvix/eval/src/tests/nix_tests/eval-okay-patterns.nix
new file mode 100644
index 000000000000..96fd25a01517
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-patterns.nix
@@ -0,0 +1,16 @@
+let
+
+  f = args@{x, y, z}: x + args.y + z;
+
+  g = {x, y, z}@args: f args;
+
+  h = {x ? "d", y ? x, z ? args.x}@args: x + y + z;
+
+  j = {x, y, z, ...}: x + y + z;
+
+in
+  f {x = "a"; y = "b"; z = "c";} +
+  g {x = "x"; y = "y"; z = "z";} +
+  h {x = "D";} +
+  h {x = "D"; y = "E"; z = "F";} +
+  j {x = "i"; y = "j"; z = "k"; bla = "bla"; foo = "bar";}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-readfile.exp b/tvix/eval/src/tests/nix_tests/eval-okay-readfile.exp
new file mode 100644
index 000000000000..a2c87d0c439f
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-readfile.exp
@@ -0,0 +1 @@
+"builtins.readFile ./eval-okay-readfile.nix\n"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-readfile.nix b/tvix/eval/src/tests/nix_tests/eval-okay-readfile.nix
new file mode 100644
index 000000000000..82f7cb17435a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-readfile.nix
@@ -0,0 +1 @@
+builtins.readFile ./eval-okay-readfile.nix
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.exp b/tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.exp
new file mode 100644
index 000000000000..c508d5366f70
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.exp
@@ -0,0 +1 @@
+false
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.nix b/tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.nix
new file mode 100644
index 000000000000..df9fc3f37d22
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-redefine-builtin.nix
@@ -0,0 +1,3 @@
+let
+  throw = abort "Error!";
+in (builtins.tryEval <foobaz>).success
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regex-match.exp b/tvix/eval/src/tests/nix_tests/eval-okay-regex-match.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regex-match.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regex-match.nix b/tvix/eval/src/tests/nix_tests/eval-okay-regex-match.nix
new file mode 100644
index 000000000000..273e2590713e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regex-match.nix
@@ -0,0 +1,29 @@
+with builtins;
+
+let
+
+  matches = pat: s: match pat s != null;
+
+  splitFN = match "((.*)/)?([^/]*)\\.(nix|cc)";
+
+in
+
+assert  matches "foobar" "foobar";
+assert  matches "fo*" "f";
+assert !matches "fo+" "f";
+assert  matches "fo*" "fo";
+assert  matches "fo*" "foo";
+assert  matches "fo+" "foo";
+assert  matches "fo{1,2}" "foo";
+assert !matches "fo{1,2}" "fooo";
+assert !matches "fo*" "foobar";
+assert  matches "[[:space:]]+([^[:space:]]+)[[:space:]]+" "  foo   ";
+assert !matches "[[:space:]]+([[:upper:]]+)[[:space:]]+" "  foo   ";
+
+assert match "(.*)\\.nix" "foobar.nix" == [ "foobar" ];
+assert match "[[:space:]]+([[:upper:]]+)[[:space:]]+" "  FOO   " == [ "FOO" ];
+
+assert splitFN "/path/to/foobar.nix" == [ "/path/to/" "/path/to" "foobar" "nix" ];
+assert splitFN "foobar.cc" == [ null null "foobar" "cc" ];
+
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regex-split.exp b/tvix/eval/src/tests/nix_tests/eval-okay-regex-split.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regex-split.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regex-split.nix b/tvix/eval/src/tests/nix_tests/eval-okay-regex-split.nix
new file mode 100644
index 000000000000..0073e057787d
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regex-split.nix
@@ -0,0 +1,48 @@
+with builtins;
+
+# Non capturing regex returns empty lists
+assert  split "foobar" "foobar"  == ["" [] ""];
+assert  split "fo*" "f"          == ["" [] ""];
+assert  split "fo+" "f"          == ["f"];
+assert  split "fo*" "fo"         == ["" [] ""];
+assert  split "fo*" "foo"        == ["" [] ""];
+assert  split "fo+" "foo"        == ["" [] ""];
+assert  split "fo{1,2}" "foo"    == ["" [] ""];
+assert  split "fo{1,2}" "fooo"   == ["" [] "o"];
+assert  split "fo*" "foobar"     == ["" [] "bar"];
+
+# Capturing regex returns a list of sub-matches
+assert  split "(fo*)" "f"        == ["" ["f"] ""];
+assert  split "(fo+)" "f"        == ["f"];
+assert  split "(fo*)" "fo"       == ["" ["fo"] ""];
+assert  split "(f)(o*)" "f"      == ["" ["f" ""] ""];
+assert  split "(f)(o*)" "foo"    == ["" ["f" "oo"] ""];
+assert  split "(fo+)" "foo"      == ["" ["foo"] ""];
+assert  split "(fo{1,2})" "foo"  == ["" ["foo"] ""];
+assert  split "(fo{1,2})" "fooo" == ["" ["foo"] "o"];
+assert  split "(fo*)" "foobar"   == ["" ["foo"] "bar"];
+
+# Matches are greedy.
+assert  split "(o+)" "oooofoooo" == ["" ["oooo"] "f" ["oooo"] ""];
+
+# Matches multiple times.
+assert  split "(b)" "foobarbaz"  == ["foo" ["b"] "ar" ["b"] "az"];
+
+# Split large strings containing newlines. null are inserted when a
+# pattern within the current did not match anything.
+assert  split "[[:space:]]+|([',.!?])" ''
+  Nix Rocks!
+  That's why I use it.
+''  == [
+  "Nix" [ null ] "Rocks" ["!"] "" [ null ]
+  "That" ["'"] "s" [ null ] "why" [ null ] "I" [ null ] "use" [ null ] "it" ["."] "" [ null ]
+  ""
+];
+
+# Documentation examples
+assert  split  "(a)b" "abc"      == [ "" [ "a" ] "c" ];
+assert  split  "([ac])" "abc"    == [ "" [ "a" ] "b" [ "c" ] "" ];
+assert  split  "(a)|(c)" "abc"   == [ "" [ "a" null ] "b" [ null "c" ] "" ];
+assert  split  "([[:upper:]]+)" "  FOO   " == [ "  " [ "FOO" ] "   " ];
+
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.exp b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.exp
new file mode 100644
index 000000000000..00750edc07d6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.exp
@@ -0,0 +1 @@
+3
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.nix b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.nix
new file mode 100644
index 000000000000..694e9a13b7c5
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220122.nix
@@ -0,0 +1 @@
+((_: _) 1) + ((__: __) 2)
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.exp b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.exp
new file mode 100644
index 000000000000..00750edc07d6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.exp
@@ -0,0 +1 @@
+3
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.nix b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.nix
new file mode 100644
index 000000000000..48550237394a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-regression-20220125.nix
@@ -0,0 +1,2 @@
+((__curPosFoo: __curPosFoo) 1) + ((__curPosBar: __curPosBar) 2)
+
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-remove.exp b/tvix/eval/src/tests/nix_tests/eval-okay-remove.exp
new file mode 100644
index 000000000000..8d38505c1686
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-remove.exp
@@ -0,0 +1 @@
+456
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-remove.nix b/tvix/eval/src/tests/nix_tests/eval-okay-remove.nix
new file mode 100644
index 000000000000..4ad5ba897fa7
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-remove.nix
@@ -0,0 +1,5 @@
+let {
+  attrs = {x = 123; y = 456;};
+
+  body = (removeAttrs attrs ["x"]).y;
+}
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.exp b/tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.exp
new file mode 100644
index 000000000000..72e8274d8c58
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.exp
@@ -0,0 +1 @@
+[ "faabar" "fbar" "fubar" "faboor" "fubar" "XaXbXcX" "X" "a_b" ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.nix b/tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.nix
new file mode 100644
index 000000000000..bd8031fc004e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-replacestrings.nix
@@ -0,0 +1,11 @@
+with builtins;
+
+[ (replaceStrings ["o"] ["a"] "foobar")
+  (replaceStrings ["o"] [""] "foobar")
+  (replaceStrings ["oo"] ["u"] "foobar")
+  (replaceStrings ["oo" "a"] ["a" "oo"] "foobar")
+  (replaceStrings ["oo" "oo"] ["u" "i"] "foobar")
+  (replaceStrings [""] ["X"] "abc")
+  (replaceStrings [""] ["X"] "")
+  (replaceStrings ["-"] ["_"] "a-b")
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-1.exp b/tvix/eval/src/tests/nix_tests/eval-okay-scope-1.exp
new file mode 100644
index 000000000000..00750edc07d6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-1.exp
@@ -0,0 +1 @@
+3
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-1.nix b/tvix/eval/src/tests/nix_tests/eval-okay-scope-1.nix
new file mode 100644
index 000000000000..fa38a7174e03
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-1.nix
@@ -0,0 +1,6 @@
+(({x}: x:
+
+  { x = 1;
+    y = x;
+  }
+) {x = 2;} 3).y
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-2.exp b/tvix/eval/src/tests/nix_tests/eval-okay-scope-2.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-2.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-2.nix b/tvix/eval/src/tests/nix_tests/eval-okay-scope-2.nix
new file mode 100644
index 000000000000..eb8b02bc4994
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-2.nix
@@ -0,0 +1,6 @@
+((x: {x}:
+  rec {
+    x = 1;
+    y = x;
+  }
+) 2 {x = 3;}).y
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-3.exp b/tvix/eval/src/tests/nix_tests/eval-okay-scope-3.exp
new file mode 100644
index 000000000000..b8626c4cff28
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-3.exp
@@ -0,0 +1 @@
+4
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-3.nix b/tvix/eval/src/tests/nix_tests/eval-okay-scope-3.nix
new file mode 100644
index 000000000000..10d6bc04d830
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-3.nix
@@ -0,0 +1,6 @@
+((x: as: {x}:
+  rec {
+    inherit (as) x;
+    y = x;
+  }
+) 2 {x = 4;} {x = 3;}).y
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-4.exp b/tvix/eval/src/tests/nix_tests/eval-okay-scope-4.exp
new file mode 100644
index 000000000000..00ff03a46c9b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-4.exp
@@ -0,0 +1 @@
+"ccdd"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-4.nix b/tvix/eval/src/tests/nix_tests/eval-okay-scope-4.nix
new file mode 100644
index 000000000000..dc8243bc8546
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-4.nix
@@ -0,0 +1,10 @@
+let {
+
+  x = "a";
+  y = "b";
+
+  f = {x ? y, y ? x}: x + y;
+
+  body = f {x = "c";} + f {y = "d";};
+
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-6.exp b/tvix/eval/src/tests/nix_tests/eval-okay-scope-6.exp
new file mode 100644
index 000000000000..00ff03a46c9b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-6.exp
@@ -0,0 +1 @@
+"ccdd"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-6.nix b/tvix/eval/src/tests/nix_tests/eval-okay-scope-6.nix
new file mode 100644
index 000000000000..0995d4e7e7e0
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-6.nix
@@ -0,0 +1,7 @@
+let {
+
+  f = {x ? y, y ? x}: x + y;
+
+  body = f {x = "c";} + f {y = "d";};
+
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-7.exp b/tvix/eval/src/tests/nix_tests/eval-okay-scope-7.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-7.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-scope-7.nix b/tvix/eval/src/tests/nix_tests/eval-okay-scope-7.nix
new file mode 100644
index 000000000000..4da02968f6b7
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-scope-7.nix
@@ -0,0 +1,6 @@
+rec {
+  inherit (x) y;
+  x = {
+    y = 1;
+  };
+}.y
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-seq.exp b/tvix/eval/src/tests/nix_tests/eval-okay-seq.exp
new file mode 100644
index 000000000000..0cfbf08886fc
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-seq.exp
@@ -0,0 +1 @@
+2
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-seq.nix b/tvix/eval/src/tests/nix_tests/eval-okay-seq.nix
new file mode 100644
index 000000000000..0a9a21c03b62
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-seq.nix
@@ -0,0 +1 @@
+builtins.seq 1 2
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-sort.exp b/tvix/eval/src/tests/nix_tests/eval-okay-sort.exp
new file mode 100644
index 000000000000..899119e20e38
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-sort.exp
@@ -0,0 +1 @@
+[ [ 42 77 147 249 483 526 ] [ 526 483 249 147 77 42 ] [ "bar" "fnord" "foo" "xyzzy" ] [ { key = 1; value = "foo"; } { key = 1; value = "fnord"; } { key = 2; value = "bar"; } ] [ [ ] [ ] [ 1 ] [ 1 4 ] [ 1 5 ] [ 1 6 ] [ 2 ] [ 2 3 ] [ 3 ] [ 3 ] ] ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-sort.nix b/tvix/eval/src/tests/nix_tests/eval-okay-sort.nix
new file mode 100644
index 000000000000..50aa78e40325
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-sort.nix
@@ -0,0 +1,20 @@
+with builtins;
+
+[ (sort lessThan [ 483 249 526 147 42 77 ])
+  (sort (x: y: y < x) [ 483 249 526 147 42 77 ])
+  (sort lessThan [ "foo" "bar" "xyzzy" "fnord" ])
+  (sort (x: y: x.key < y.key)
+    [ { key = 1; value = "foo"; } { key = 2; value = "bar"; } { key = 1; value = "fnord"; } ])
+  (sort lessThan [
+    [ 1 6 ]
+    [ ]
+    [ 2 3 ]
+    [ 3 ]
+    [ 1 5 ]
+    [ 2 ]
+    [ 1 ]
+    [ ]
+    [ 1 4 ]
+    [ 3 ]
+  ])
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-splitversion.exp b/tvix/eval/src/tests/nix_tests/eval-okay-splitversion.exp
new file mode 100644
index 000000000000..153ceb8186a0
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-splitversion.exp
@@ -0,0 +1 @@
+[ "1" "2" "3" ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-splitversion.nix b/tvix/eval/src/tests/nix_tests/eval-okay-splitversion.nix
new file mode 100644
index 000000000000..9e5c99d2e7f6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-splitversion.nix
@@ -0,0 +1 @@
+builtins.splitVersion "1.2.3"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-string.exp b/tvix/eval/src/tests/nix_tests/eval-okay-string.exp
new file mode 100644
index 000000000000..63f650f73a3a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-string.exp
@@ -0,0 +1 @@
+"foobar/a/b/c/d/foo/xyzzy/foo.txt/../foo/x/yescape: \"quote\" \n \\end\nof\nlinefoobarblaatfoo$bar$\"$\"$"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-string.nix b/tvix/eval/src/tests/nix_tests/eval-okay-string.nix
new file mode 100644
index 000000000000..47cc989ad46a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-string.nix
@@ -0,0 +1,12 @@
+"foo" + "bar"
+  + toString (/a/b + /c/d)
+  + toString (/foo/bar + "/../xyzzy/." + "/foo.txt")
+  + ("/../foo" + toString /x/y)
+  + "escape: \"quote\" \n \\"
+  + "end
+of
+line"
+  + "foo${if true then "b${"a" + "r"}" else "xyzzy"}blaat"
+  + "foo$bar"
+  + "$\"$\""
+  + "$"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.exp b/tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.nix b/tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.nix
new file mode 100644
index 000000000000..5e40928dbe31
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-strings-as-attrs-names.nix
@@ -0,0 +1,20 @@
+let
+
+  attr = {
+    "key 1" = "test";
+    "key 2" = "caseok";
+  };
+
+  t1 = builtins.getAttr "key 1" attr;
+  t2 = attr."key 2";
+  t3 = attr ? "key 1";
+  t4 = builtins.attrNames { inherit (attr) "key 1"; };
+
+  # This is permitted, but there is currently no way to reference this
+  # variable.
+  "foo bar" = 1;
+
+in t1 == "test"
+   && t2 == "caseok"
+   && t3 == true
+   && t4 == ["key 1"]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-substring.exp b/tvix/eval/src/tests/nix_tests/eval-okay-substring.exp
new file mode 100644
index 000000000000..6aace04b0f57
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-substring.exp
@@ -0,0 +1 @@
+"ooxfoobarybarzobaabbc"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-substring.nix b/tvix/eval/src/tests/nix_tests/eval-okay-substring.nix
new file mode 100644
index 000000000000..424af00d9b3b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-substring.nix
@@ -0,0 +1,21 @@
+with builtins;
+
+let
+
+  s = "foobar";
+
+in
+
+substring 1 2 s
++ "x"
++ substring 0 (stringLength s) s
++ "y"
++ substring 3 100 s
++ "z"
++ substring 2 (sub (stringLength s) 3) s
++ "a"
++ substring 3 0 s
++ "b"
++ substring 3 1 s
++ "c"
++ substring 5 10 "perl"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.exp b/tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.exp
new file mode 100644
index 000000000000..f7393e847d34
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.exp
@@ -0,0 +1 @@
+100000
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.nix b/tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.nix
new file mode 100644
index 000000000000..a3962ce3fdb5
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-tail-call-1.nix
@@ -0,0 +1,3 @@
+let
+  f = n: if n == 100000 then n else f (n + 1);
+in f 0
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-tojson.exp b/tvix/eval/src/tests/nix_tests/eval-okay-tojson.exp
new file mode 100644
index 000000000000..e92aae3235f2
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-tojson.exp
@@ -0,0 +1 @@
+"{\"a\":123,\"b\":-456,\"c\":\"foo\",\"d\":\"foo\\n\\\"bar\\\"\",\"e\":true,\"f\":false,\"g\":[1,2,3],\"h\":[\"a\",[\"b\",{\"foo\\nbar\":{}}]],\"i\":3,\"j\":1.44,\"k\":\"foo\"}"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-tojson.nix b/tvix/eval/src/tests/nix_tests/eval-okay-tojson.nix
new file mode 100644
index 000000000000..ce67943bead5
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-tojson.nix
@@ -0,0 +1,13 @@
+builtins.toJSON
+  { a = 123;
+    b = -456;
+    c = "foo";
+    d = "foo\n\"bar\"";
+    e = true;
+    f = false;
+    g = [ 1 2 3 ];
+    h = [ "a" [ "b" { "foo\nbar" = {}; } ] ];
+    i = 1 + 2;
+    j = 1.44;
+    k = { __toString = self: self.a; a = "foo"; };
+  }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp
new file mode 100644
index 000000000000..828220890ecd
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp
@@ -0,0 +1 @@
+"<?xml version='1.0' encoding='utf-8'?>\n<expr>\n  <attrs>\n    <attr name=\"a\">\n      <string value=\"s\" />\n    </attr>\n  </attrs>\n</expr>\n"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix
new file mode 100644
index 000000000000..068c97a6c1b3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix
@@ -0,0 +1,3 @@
+# Make sure the expected XML output is produced; in particular, make sure it
+# doesn't contain source location information.
+builtins.toXML { a = "s"; }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp
new file mode 100644
index 000000000000..634a841eb190
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp
@@ -0,0 +1 @@
+"<?xml version='1.0' encoding='utf-8'?>\n<expr>\n  <list>\n    <string value=\"ab\" />\n    <int value=\"10\" />\n    <attrs>\n      <attr name=\"x\">\n        <string value=\"x\" />\n      </attr>\n      <attr name=\"y\">\n        <string value=\"x\" />\n      </attr>\n    </attrs>\n  </list>\n</expr>\n"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix
new file mode 100644
index 000000000000..ff1791b30eb5
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix
@@ -0,0 +1 @@
+builtins.toXML [("a" + "b") 10 (rec {x = "x"; y = x;})]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-tryeval.exp b/tvix/eval/src/tests/nix_tests/eval-okay-tryeval.exp
new file mode 100644
index 000000000000..2b2e6fa711f4
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-tryeval.exp
@@ -0,0 +1 @@
+{ x = { success = true; value = "x"; }; y = { success = false; value = false; }; z = { success = false; value = false; }; }
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-tryeval.nix b/tvix/eval/src/tests/nix_tests/eval-okay-tryeval.nix
new file mode 100644
index 000000000000..629bc440a85a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-tryeval.nix
@@ -0,0 +1,5 @@
+{
+  x = builtins.tryEval "x";
+  y = builtins.tryEval (assert false; "y");
+  z = builtins.tryEval (throw "bla");
+}
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-types.exp b/tvix/eval/src/tests/nix_tests/eval-okay-types.exp
new file mode 100644
index 000000000000..92a15329935a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-types.exp
@@ -0,0 +1 @@
+[ true false true false true false true false true true true true true true true true true true true false true true true false "int" "bool" "string" "null" "set" "list" "lambda" "lambda" "lambda" "lambda" ]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-types.nix b/tvix/eval/src/tests/nix_tests/eval-okay-types.nix
new file mode 100644
index 000000000000..9b58be5d1dd4
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-types.nix
@@ -0,0 +1,37 @@
+with builtins;
+
+[ (isNull null)
+  (isNull (x: x))
+  (isFunction (x: x))
+  (isFunction "fnord")
+  (isString ("foo" + "bar"))
+  (isString [ "x" ])
+  (isInt (1 + 2))
+  (isInt { x = 123; })
+  (isInt (1 / 2))
+  (isInt (1 + 1))
+  (isInt (1 / 2))
+  (isInt (1 * 2))
+  (isInt (1 - 2))
+  (isFloat (1.2))
+  (isFloat (1 + 1.0))
+  (isFloat (1 / 2.0))
+  (isFloat (1 * 2.0))
+  (isFloat (1 - 2.0))
+  (isBool (true && false))
+  (isBool null)
+  (isPath /nix/store)
+  (isPath ./.)
+  (isAttrs { x = 123; })
+  (isAttrs null)
+  (typeOf (3 * 4))
+  (typeOf true)
+  (typeOf "xyzzy")
+  (typeOf null)
+  (typeOf { x = 456; })
+  (typeOf [ 1 2 3 ])
+  (typeOf (x: x))
+  (typeOf ((x: y: x) 1))
+  (typeOf map)
+  (typeOf (map (x: x)))
+]
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-versions.exp b/tvix/eval/src/tests/nix_tests/eval-okay-versions.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-versions.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-versions.nix b/tvix/eval/src/tests/nix_tests/eval-okay-versions.nix
new file mode 100644
index 000000000000..e9111f5f4331
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-versions.nix
@@ -0,0 +1,43 @@
+let
+
+  name1 = "hello-1.0.2";
+  name2 = "hello";
+  name3 = "915resolution-0.5.2";
+  name4 = "xf86-video-i810-1.7.4";
+  name5 = "name-that-ends-with-dash--1.0";
+
+  eq = 0;
+  lt = builtins.sub 0 1;
+  gt = 1;
+
+  versionTest = v1: v2: expected:
+    let d1 = builtins.compareVersions v1 v2;
+        d2 = builtins.compareVersions v2 v1;
+    in d1 == builtins.sub 0 d2 && d1 == expected;
+
+  tests = [
+    ((builtins.parseDrvName name1).name == "hello")
+    ((builtins.parseDrvName name1).version == "1.0.2")
+    ((builtins.parseDrvName name2).name == "hello")
+    ((builtins.parseDrvName name2).version == "")
+    ((builtins.parseDrvName name3).name == "915resolution")
+    ((builtins.parseDrvName name3).version == "0.5.2")
+    ((builtins.parseDrvName name4).name == "xf86-video-i810")
+    ((builtins.parseDrvName name4).version == "1.7.4")
+    ((builtins.parseDrvName name5).name == "name-that-ends-with-dash")
+    ((builtins.parseDrvName name5).version == "-1.0")
+    (versionTest "1.0" "2.3" lt)
+    (versionTest "2.1" "2.3" lt)
+    (versionTest "2.3" "2.3" eq)
+    (versionTest "2.5" "2.3" gt)
+    (versionTest "3.1" "2.3" gt)
+    (versionTest "2.3.1" "2.3" gt)
+    (versionTest "2.3.1" "2.3a" gt)
+    (versionTest "2.3pre1" "2.3" lt)
+    (versionTest "2.3pre3" "2.3pre12" lt)
+    (versionTest "2.3a" "2.3c" lt)
+    (versionTest "2.3pre1" "2.3c" lt)
+    (versionTest "2.3pre1" "2.3q" lt)
+  ];
+
+in (import ./lib.nix).and tests
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-with.exp b/tvix/eval/src/tests/nix_tests/eval-okay-with.exp
new file mode 100644
index 000000000000..378c8dc80403
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-with.exp
@@ -0,0 +1 @@
+"xyzzybarxyzzybar"
diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-with.nix b/tvix/eval/src/tests/nix_tests/eval-okay-with.nix
new file mode 100644
index 000000000000..033e8d3aba57
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-with.nix
@@ -0,0 +1,19 @@
+let {
+
+  a = "xyzzy";
+
+  as = {
+    a = "foo";
+    b = "bar";
+  };
+
+  bs = {
+    a = "bar";
+  };
+
+  x = with as; a + b;
+
+  y = with as; with bs; a + b;
+
+  body = x + y;
+}
diff --git a/tvix/eval/src/tests/nix_tests/imported.nix b/tvix/eval/src/tests/nix_tests/imported.nix
new file mode 100644
index 000000000000..fb39ee4efacd
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/imported.nix
@@ -0,0 +1,3 @@
+# The function ‘range’ comes from lib.nix and was added to the lexical
+# scope by scopedImport.
+range 1 5 ++ import ./imported2.nix
diff --git a/tvix/eval/src/tests/nix_tests/imported2.nix b/tvix/eval/src/tests/nix_tests/imported2.nix
new file mode 100644
index 000000000000..6d0a2992b732
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/imported2.nix
@@ -0,0 +1 @@
+range 6 10
diff --git a/tvix/eval/src/tests/nix_tests/lib.nix b/tvix/eval/src/tests/nix_tests/lib.nix
new file mode 100644
index 000000000000..028a538314b7
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/lib.nix
@@ -0,0 +1,61 @@
+with builtins;
+
+rec {
+
+  fold = op: nul: list:
+    if list == []
+    then nul
+    else op (head list) (fold op nul (tail list));
+
+  concat =
+    fold (x: y: x + y) "";
+
+  and = fold (x: y: x && y) true;
+
+  flatten = x:
+    if isList x
+    then fold (x: y: (flatten x) ++ y) [] x
+    else [x];
+
+  sum = foldl' (x: y: add x y) 0;
+
+  hasSuffix = ext: fileName:
+    let lenFileName = stringLength fileName;
+        lenExt = stringLength ext;
+    in !(lessThan lenFileName lenExt) &&
+       substring (sub lenFileName lenExt) lenFileName fileName == ext;
+
+  # Split a list at the given position.
+  splitAt = pos: list:
+    if pos == 0 then {first = []; second = list;} else
+    if list == [] then {first = []; second = [];} else
+    let res = splitAt (sub pos 1) (tail list);
+    in {first = [(head list)] ++ res.first; second = res.second;};
+
+  # Stable merge sort.
+  sortBy = comp: list:
+    if lessThan 1 (length list)
+    then
+      let
+        split = splitAt (div (length list) 2) list;
+        first = sortBy comp split.first;
+        second = sortBy comp split.second;
+      in mergeLists comp first second
+    else list;
+
+  mergeLists = comp: list1: list2:
+    if list1 == [] then list2 else
+    if list2 == [] then list1 else
+    if comp (head list2) (head list1) then [(head list2)] ++ mergeLists comp list1 (tail list2) else
+    [(head list1)] ++ mergeLists comp (tail list1) list2;
+
+  id = x: x;
+
+  const = x: y: x;
+
+  range = first: last:
+    if first > last
+      then []
+      else genList (n: first + n) (last - first + 1);
+
+}
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-bad-antiquote-2.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-bad-antiquote-2.nix
new file mode 100644
index 000000000000..3745235ce95e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-bad-antiquote-2.nix
@@ -0,0 +1 @@
+"${./fnord}"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-nonexist-path.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-nonexist-path.nix
new file mode 100644
index 000000000000..f2f08107b516
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-nonexist-path.nix
@@ -0,0 +1,4 @@
+# This must fail to evaluate, since ./fnord doesn't exist.  If it did
+# exist, it would produce "/nix/store/<hash>-fnord/xyzzy" (with an
+# appropriate context).
+"${./fnord}/xyzzy"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-scope-5.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-scope-5.nix
new file mode 100644
index 000000000000..f89a65a99be3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-scope-5.nix
@@ -0,0 +1,10 @@
+let {
+
+  x = "a";
+  y = "b";
+
+  f = {x ? y, y ? x}: x + y;
+
+  body = f {};
+
+}
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-undeclared-arg.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-undeclared-arg.nix
new file mode 100644
index 000000000000..cafdf1636272
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-fail-undeclared-arg.nix
@@ -0,0 +1 @@
+({x, z}: x + z) {x = "foo"; y = "bla"; z = "bar";}
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.exp
new file mode 100644
index 000000000000..b46938032e73
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.exp
@@ -0,0 +1 @@
+{ __overrides = { bar = "qux"; }; bar = "qux"; foo = "bar"; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.nix
new file mode 100644
index 000000000000..2e5c85483be6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-attrs6.nix
@@ -0,0 +1,4 @@
+rec {
+  "${"foo"}" = "bar";
+   __overrides = { bar = "qux"; };
+}
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.exp
new file mode 100644
index 000000000000..7a8391786a09
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.exp
@@ -0,0 +1 @@
+"xyzzy!xyzzy!foobar"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.flags b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.flags
new file mode 100644
index 000000000000..217c7a5ae291
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.flags
@@ -0,0 +1 @@
+--arg lib import(nix_tests/lib.nix) --argstr xyzzy xyzzy! -A result
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.nix
new file mode 100644
index 000000000000..815f51b1d67a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-autoargs.nix
@@ -0,0 +1,15 @@
+let
+
+  foobar = "foobar";
+
+in
+
+{ xyzzy2 ? xyzzy # mutually recursive args
+, xyzzy ? "blaat" # will be overridden by --argstr
+, fb ? foobar
+, lib # will be set by --arg
+}:
+
+{
+  result = lib.concat [xyzzy xyzzy2 fb];
+}
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-closure.exp.xml b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-closure.exp.xml
new file mode 100644
index 000000000000..dffc03a99891
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-closure.exp.xml
@@ -0,0 +1,343 @@
+<?xml version='1.0' encoding='utf-8'?>
+<expr>
+  <list>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-13" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-12" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-11" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-9" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-8" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-7" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-5" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-4" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="-3" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="-1" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="0" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="1" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="2" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="4" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="5" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="6" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="8" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="9" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="10" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="13" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="14" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="15" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="17" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="18" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="19" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="22" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="23" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="26" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="27" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="28" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="31" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="32" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="35" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="36" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="40" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="41" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="44" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="45" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="49" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="53" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="54" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="58" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="62" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="foo">
+        <bool value="true" />
+      </attr>
+      <attr name="key">
+        <int value="67" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="71" />
+      </attr>
+    </attrs>
+    <attrs>
+      <attr name="key">
+        <int value="80" />
+      </attr>
+    </attrs>
+  </list>
+</expr>
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.nix
new file mode 100644
index 000000000000..43178bd2eef9
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context-introspection.nix
@@ -0,0 +1,24 @@
+let
+  drv = derivation {
+    name = "fail";
+    builder = "/bin/false";
+    system = "x86_64-linux";
+    outputs = [ "out" "foo" ];
+  };
+
+  path = "${./eval-okay-context-introspection.nix}";
+
+  desired-context = {
+    "${builtins.unsafeDiscardStringContext path}" = {
+      path = true;
+    };
+    "${builtins.unsafeDiscardStringContext drv.drvPath}" = {
+      outputs = [ "foo" "out" ];
+      allOutputs = true;
+    };
+  };
+
+  legit-context = builtins.getContext "${path}${drv.outPath}${drv.foo.outPath}${drv.drvPath}";
+
+  constructed-context = builtins.getContext (builtins.appendContext "" desired-context);
+in legit-context == constructed-context
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.exp
new file mode 100644
index 000000000000..2f535bdbc454
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.exp
@@ -0,0 +1 @@
+"foo eval-okay-context.nix bar"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.nix
new file mode 100644
index 000000000000..7b9531cfe9e1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-context.nix
@@ -0,0 +1,6 @@
+let s = "foo ${builtins.substring 33 100 (baseNameOf "${./eval-okay-context.nix}")} bar";
+in
+  if s != "foo eval-okay-context.nix bar"
+  then abort "context not discarded"
+  else builtins.unsafeDiscardStringContext s
+
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.exp
new file mode 100644
index 000000000000..65fd65b4d01f
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.exp
@@ -0,0 +1 @@
+[ 3 7 4 9 ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.nix
new file mode 100644
index 000000000000..b79553df0bd3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-curpos.nix
@@ -0,0 +1,5 @@
+# Bla
+let
+  x = __curPos;
+    y = __curPos;
+in [ x.line x.column y.line y.column ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.exp
new file mode 100644
index 000000000000..8e7c61ab8e77
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.exp
@@ -0,0 +1 @@
+"b-overridden b-overridden a"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.nix
new file mode 100644
index 000000000000..3fb023e1cd42
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-delayed-with.nix
@@ -0,0 +1,29 @@
+let
+
+  pkgs_ = with pkgs; {
+    a = derivation {
+      name = "a";
+      system = builtins.currentSystem;
+      builder = "/bin/sh";
+      args = [ "-c" "touch $out" ];
+      inherit b;
+    };
+
+    b = derivation {
+      name = "b";
+      system = builtins.currentSystem;
+      builder = "/bin/sh";
+      args = [ "-c" "touch $out" ];
+      inherit a;
+    };
+
+    c = b;
+  };
+
+  packageOverrides = pkgs: with pkgs; {
+    b = derivation (b.drvAttrs // { name = "${b.name}-overridden"; });
+  };
+
+  pkgs = pkgs_ // (packageOverrides pkgs_);
+
+in "${pkgs.a.b.name} ${pkgs.c.name} ${pkgs.b.a.name}"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.exp
new file mode 100644
index 000000000000..ec04aab6aeec
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.exp
@@ -0,0 +1 @@
+[ true true true false ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.nix
new file mode 100644
index 000000000000..d526cb4a2161
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-eq-derivations.nix
@@ -0,0 +1,10 @@
+let
+
+  drvA1 = derivation { name = "a"; builder = "/foo"; system = "i686-linux"; };
+  drvA2 = derivation { name = "a"; builder = "/foo"; system = "i686-linux"; };
+  drvA3 = derivation { name = "a"; builder = "/foo"; system = "i686-linux"; } // { dummy = 1; };
+  
+  drvC1 = derivation { name = "c"; builder = "/foo"; system = "i686-linux"; };
+  drvC2 = derivation { name = "c"; builder = "/bar"; system = "i686-linux"; };
+
+in [ (drvA1 == drvA1) (drvA1 == drvA2) (drvA1 == drvA3) (drvC1 == drvC2) ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.exp
new file mode 100644
index 000000000000..81f80420b996
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.exp
@@ -0,0 +1 @@
+"23;24;23;23"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.nix
new file mode 100644
index 000000000000..d76a0d86ea78
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-floor-ceil.nix
@@ -0,0 +1,9 @@
+with import ./lib.nix;
+
+let
+  n1 = builtins.floor 23.5;
+  n2 = builtins.ceil 23.5;
+  n3 = builtins.floor 23;
+  n4 = builtins.ceil 23;
+in
+  builtins.concatStringsSep ";" (map toString [ n1 n2 n3 n4 ])
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.exp.xml b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.exp.xml
new file mode 100644
index 000000000000..651f54c36341
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.exp.xml
@@ -0,0 +1,15 @@
+<?xml version='1.0' encoding='utf-8'?>
+<expr>
+  <list>
+    <string value="stdenv" />
+    <string value="fetchurl" />
+    <string value="aterm-stdenv" />
+    <string value="aterm-stdenv2" />
+    <string value="libX11" />
+    <string value="libXv" />
+    <string value="mplayer-stdenv2.libXv-libX11" />
+    <string value="mplayer-stdenv2.libXv-libX11_2" />
+    <string value="nix-stdenv-aterm-stdenv" />
+    <string value="nix-stdenv2-aterm2-stdenv2" />
+  </list>
+</expr>
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.nix
new file mode 100644
index 000000000000..68dca62ee18d
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-functionargs.nix
@@ -0,0 +1,80 @@
+let
+
+  stdenvFun = { }: { name = "stdenv"; };
+  stdenv2Fun = { }: { name = "stdenv2"; };
+  fetchurlFun = { stdenv }: assert stdenv.name == "stdenv"; { name = "fetchurl"; };
+  atermFun = { stdenv, fetchurl }: { name = "aterm-${stdenv.name}"; };
+  aterm2Fun = { stdenv, fetchurl }: { name = "aterm2-${stdenv.name}"; };
+  nixFun = { stdenv, fetchurl, aterm }: { name = "nix-${stdenv.name}-${aterm.name}"; };
+  
+  mplayerFun =
+    { stdenv, fetchurl, enableX11 ? false, xorg ? null, enableFoo ? true, foo ? null  }:
+    assert stdenv.name == "stdenv2";
+    assert enableX11 -> xorg.libXv.name == "libXv";
+    assert enableFoo -> foo != null;
+    { name = "mplayer-${stdenv.name}.${xorg.libXv.name}-${xorg.libX11.name}"; };
+
+  makeOverridable = f: origArgs: f origArgs //
+    { override = newArgs:
+        makeOverridable f (origArgs // (if builtins.isFunction newArgs then newArgs origArgs else newArgs));
+    };
+    
+  callPackage_ = pkgs: f: args:
+    makeOverridable f ((builtins.intersectAttrs (builtins.functionArgs f) pkgs) // args);
+
+  allPackages =
+    { overrides ? (pkgs: pkgsPrev: { }) }:
+    let
+      callPackage = callPackage_ pkgs;
+      pkgs = pkgsStd // (overrides pkgs pkgsStd);
+      pkgsStd = {
+        inherit pkgs;
+        stdenv = callPackage stdenvFun { };
+        stdenv2 = callPackage stdenv2Fun { };
+        fetchurl = callPackage fetchurlFun { };
+        aterm = callPackage atermFun { };
+        xorg = callPackage xorgFun { };
+        mplayer = callPackage mplayerFun { stdenv = pkgs.stdenv2; enableFoo = false; };
+        nix = callPackage nixFun { };
+      };
+    in pkgs;
+
+  libX11Fun = { stdenv, fetchurl }: { name = "libX11"; };
+  libX11_2Fun = { stdenv, fetchurl }: { name = "libX11_2"; };
+  libXvFun = { stdenv, fetchurl, libX11 }: { name = "libXv"; };
+  
+  xorgFun =
+    { pkgs }:
+    let callPackage = callPackage_ (pkgs // pkgs.xorg); in
+    {
+      libX11 = callPackage libX11Fun { };
+      libXv = callPackage libXvFun { };
+    };
+
+in
+
+let
+
+  pkgs = allPackages { };
+  
+  pkgs2 = allPackages {
+    overrides = pkgs: pkgsPrev: {
+      stdenv = pkgs.stdenv2;
+      nix = pkgsPrev.nix.override { aterm = aterm2Fun { inherit (pkgs) stdenv fetchurl; }; };
+      xorg = pkgsPrev.xorg // { libX11 = libX11_2Fun { inherit (pkgs) stdenv fetchurl; }; };
+    };
+  };
+  
+in
+
+  [ pkgs.stdenv.name
+    pkgs.fetchurl.name
+    pkgs.aterm.name
+    pkgs2.aterm.name
+    pkgs.xorg.libX11.name
+    pkgs.xorg.libXv.name
+    pkgs.mplayer.name
+    pkgs2.mplayer.name
+    pkgs.nix.name
+    pkgs2.nix.name
+  ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.exp
new file mode 100644
index 000000000000..7f9ac40e81b1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.exp
@@ -0,0 +1 @@
+{ column = 11; file = "eval-okay-getattrpos-functionargs.nix"; line = 2; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.nix
new file mode 100644
index 000000000000..11d6bb0e3ac9
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-functionargs.nix
@@ -0,0 +1,4 @@
+let
+  fun = { foo }: {};
+  pos = builtins.unsafeGetAttrPos "foo" (builtins.functionArgs fun);
+in { inherit (pos) column line; file = baseNameOf pos.file; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.exp
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.exp
@@ -0,0 +1 @@
+null
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.nix
new file mode 100644
index 000000000000..14dd38f7734c
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos-undefined.nix
@@ -0,0 +1 @@
+builtins.unsafeGetAttrPos "abort" builtins
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.exp
new file mode 100644
index 000000000000..469249bbc646
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.exp
@@ -0,0 +1 @@
+{ column = 5; file = "eval-okay-getattrpos.nix"; line = 3; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.nix
new file mode 100644
index 000000000000..ca6b07961547
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getattrpos.nix
@@ -0,0 +1,6 @@
+let
+  as = {
+    foo = "bar";
+  };
+  pos = builtins.unsafeGetAttrPos "foo" as;
+in { inherit (pos) column line; file = baseNameOf pos.file; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.exp
new file mode 100644
index 000000000000..14e24d419005
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.exp
@@ -0,0 +1 @@
+"foobar"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.nix
new file mode 100644
index 000000000000..4cfec5f553d9
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-getenv.nix
@@ -0,0 +1 @@
+builtins.getEnv "TEST_VAR" + (if builtins.getEnv "NO_SUCH_VAR" == "" then "bar" else "bla")
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.exp
new file mode 100644
index 000000000000..bfca5652a59b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.exp
@@ -0,0 +1 @@
+{ "1" = [ 9 ]; "2" = [ 8 ]; "3" = [ 13 29 ]; "4" = [ 3 4 10 11 17 18 ]; "5" = [ 0 23 26 28 ]; "6" = [ 1 12 21 27 30 ]; "7" = [ 7 22 ]; "8" = [ 14 ]; "9" = [ 19 ]; b = [ 16 25 ]; c = [ 24 ]; d = [ 2 ]; e = [ 5 6 15 31 ]; f = [ 20 ]; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.nix
new file mode 100644
index 000000000000..7e0eab28b036
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-groupBy.nix
@@ -0,0 +1,5 @@
+with import ./../lib.nix;
+
+builtins.groupBy (n:
+  builtins.substring 0 1 (builtins.hashString "sha256" (toString n))
+) (range 0 31)
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.exp
new file mode 100644
index 000000000000..ff1e8293ef22
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.exp
@@ -0,0 +1 @@
+[ "d3b07384d113edec49eaa6238ad5ff00" "0f343b0931126a20f133d67c2b018a3b" "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15" "60cacbf3d72e1e7834203da608037b1bf83b40e8" "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c" "5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" "0cf9180a764aba863a67b6d72f0918bc131c6772642cb2dce5a34f0a702f9470ddc2bf125c12198b1995c233c34b4afd346c54a2334c350a948a51b6e8b4e6b6" "8efb4f73c5655351c444eb109230c556d39e2c7624e9c11abc9e3fb4b9b9254218cc5085b454a9698d085cfa92198491f07a723be4574adc70617b73eb0b6461" ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.nix
new file mode 100644
index 000000000000..8c9de66b7ecf
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashfile.nix
@@ -0,0 +1,4 @@
+let
+  paths = [ ./../data ./../binary-data ];
+in
+  builtins.concatLists (map (hash: map (builtins.hashFile hash) paths) ["md5" "sha1" "sha256" "sha512"])
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.exp
new file mode 100644
index 000000000000..d720a082ddb3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.exp
@@ -0,0 +1 @@
+[ "d41d8cd98f00b204e9800998ecf8427e" "6c69ee7f211c640419d5366cc076ae46" "bb3438fbabd460ea6dbd27d153e2233b" "da39a3ee5e6b4b0d3255bfef95601890afd80709" "cd54e8568c1b37cf1e5badb0779bcbf382212189" "6d12e10b1d331dad210e47fd25d4f260802b7e77" "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" "900a4469df00ccbfd0c145c6d1e4b7953dd0afafadd7534e3a4019e8d38fc663" "ad0387b3bd8652f730ca46d25f9c170af0fd589f42e7f23f5a9e6412d97d7e56" "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" "9d0886f8c6b389398a16257bc79780fab9831c7fc11c8ab07fa732cb7b348feade382f92617c9c5305fefba0af02ab5fd39a587d330997ff5bd0db19f7666653" "21644b72aa259e5a588cd3afbafb1d4310f4889680f6c83b9d531596a5a284f34dbebff409d23bcc86aee6bad10c891606f075c6f4755cb536da27db5693f3a7" ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.nix
new file mode 100644
index 000000000000..b0f62b245ca8
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-hashstring.nix
@@ -0,0 +1,4 @@
+let
+  strings = [ "" "text 1" "text 2" ];
+in
+  builtins.concatLists (map (hash: map (builtins.hashString hash) strings) ["md5" "sha1" "sha256" "sha512"])
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.exp
new file mode 100644
index 000000000000..c508125b55be
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.exp
@@ -0,0 +1 @@
+[ 1 2 3 4 5 6 7 8 9 10 ]
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.nix
new file mode 100644
index 000000000000..76213a9541e3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-import.nix
@@ -0,0 +1,11 @@
+let
+
+  overrides = {
+    import = fn: scopedImport overrides fn;
+
+    scopedImport = attrs: fn: scopedImport (overrides // attrs) fn;
+
+    builtins = builtins // overrides;
+  } // import ./../lib.nix;
+
+in scopedImport overrides ./../imported.nix
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.exp
new file mode 100644
index 000000000000..0cfbf08886fc
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.exp
@@ -0,0 +1 @@
+2
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.nix
new file mode 100644
index 000000000000..358742b36e22
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-overrides.nix
@@ -0,0 +1,9 @@
+let
+
+  overrides = { a = 2; };
+
+in (rec {
+  __overrides = overrides;
+  x = a;
+  a = 1;
+}).x
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-path.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-path.nix
new file mode 100644
index 000000000000..e67168cf3edf
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-path.nix
@@ -0,0 +1,7 @@
+builtins.path
+  { path = ./.;
+    filter = path: _: baseNameOf path == "data";
+    recursive = true;
+    sha256 = "1yhm3gwvg5a41yylymgblsclk95fs6jy72w0wv925mmidlhcq4sw";
+    name = "output";
+  }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.exp
new file mode 100644
index 000000000000..bf8d2c14ea4f
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.exp
@@ -0,0 +1 @@
+{ bar = "regular"; foo = "directory"; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.nix.disabled b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.nix.disabled
new file mode 100644
index 000000000000..a7ec9292aae2
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-readDir.nix.disabled
@@ -0,0 +1 @@
+builtins.readDir ./readDir
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.exp
new file mode 100644
index 000000000000..4519bc406db5
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.exp
@@ -0,0 +1 @@
+"abccX"
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.flags b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.flags
new file mode 100644
index 000000000000..a28e6821004a
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.flags
@@ -0,0 +1 @@
+-I lang/dir1 -I lang/dir2 -I dir5=lang/dir3
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.nix
new file mode 100644
index 000000000000..6fe33decc019
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-search-path.nix
@@ -0,0 +1,10 @@
+with import ./lib.nix;
+with builtins;
+
+assert isFunction (import <nix/fetchurl.nix>);
+
+assert length __nixPath == 5;
+assert length (filter (x: baseNameOf x.path == "dir4") __nixPath) == 1;
+
+import <a.nix> + import <b.nix> + import <c.nix> + import <dir5/c.nix>
+  + (let __nixPath = [ { path = ./dir2; } { path = ./dir1; } ]; in import <a.nix>)
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.exp.xml b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.exp.xml
new file mode 100644
index 000000000000..20099326cc96
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.exp.xml
@@ -0,0 +1,52 @@
+<?xml version='1.0' encoding='utf-8'?>
+<expr>
+  <attrs>
+    <attr name="a">
+      <string value="foo" />
+    </attr>
+    <attr name="at">
+      <function>
+        <attrspat name="args">
+          <attr name="x" />
+          <attr name="y" />
+          <attr name="z" />
+        </attrspat>
+      </function>
+    </attr>
+    <attr name="b">
+      <string value="bar" />
+    </attr>
+    <attr name="c">
+      <string value="foobar" />
+    </attr>
+    <attr name="ellipsis">
+      <function>
+        <attrspat ellipsis="1">
+          <attr name="x" />
+          <attr name="y" />
+          <attr name="z" />
+        </attrspat>
+      </function>
+    </attr>
+    <attr name="f">
+      <function>
+        <attrspat>
+          <attr name="x" />
+          <attr name="y" />
+          <attr name="z" />
+        </attrspat>
+      </function>
+    </attr>
+    <attr name="id">
+      <function>
+        <varpat name="x" />
+      </function>
+    </attr>
+    <attr name="x">
+      <int value="123" />
+    </attr>
+    <attr name="y">
+      <float value="567.89" />
+    </attr>
+  </attrs>
+</expr>
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.nix
new file mode 100644
index 000000000000..9ee9f8a0b4f5
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-xml.nix
@@ -0,0 +1,21 @@
+rec {
+
+  x = 123;
+
+  y = 567.890;
+
+  a = "foo";
+
+  b = "bar";
+
+  c = "foo" + "bar";
+
+  f = {z, x, y}: if y then x else z;
+
+  id = x: x;
+
+  at = args@{x, y, z}: x;
+
+  ellipsis = {x, y, z, ...}: x;
+
+}
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.exp
new file mode 100644
index 000000000000..9c0b15d22ba4
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.exp
@@ -0,0 +1 @@
+{ "0" = { n = "0"; v = [ 5 23 29 ]; }; "1" = { n = "1"; v = [ 7 30 ]; }; "2" = { n = "2"; v = [ 18 ]; }; "4" = { n = "4"; v = [ 10 ]; }; "5" = { n = "5"; v = [ 15 25 26 31 ]; }; "6" = { n = "6"; v = [ 3 14 ]; }; "7" = { n = "7"; v = [ 12 ]; }; "8" = { n = "8"; v = [ 2 6 8 9 ]; }; "9" = { n = "9"; v = [ 0 16 ]; }; a = { n = "a"; v = [ 17 21 22 27 ]; }; c = { n = "c"; v = [ 11 24 ]; }; d = { n = "d"; v = [ 4 13 28 ]; }; e = { n = "e"; v = [ 20 ]; }; f = { n = "f"; v = [ 1 19 ]; }; }
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.nix
new file mode 100644
index 000000000000..e5d4cdccb74e
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-zipAttrsWith.nix
@@ -0,0 +1,9 @@
+with import ./../lib.nix;
+
+let
+  str = builtins.hashString "sha256" "test";
+in
+builtins.zipAttrsWith
+  (n: v: { inherit n v; })
+  (map (n: { ${builtins.substring n 1 str} = n; })
+    (range 0 31))
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/readDir/bar b/tvix/eval/src/tests/nix_tests/notyetpassing/readDir/bar
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/readDir/bar
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/readDir/foo/git-hates-directories b/tvix/eval/src/tests/nix_tests/notyetpassing/readDir/foo/git-hates-directories
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/notyetpassing/readDir/foo/git-hates-directories
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-1.nix b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-1.nix
new file mode 100644
index 000000000000..2c02317d2a19
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-1.nix
@@ -0,0 +1,4 @@
+{ x = 123;
+  y = 456;
+  x = 789;
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-2.nix b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-2.nix
new file mode 100644
index 000000000000..864d9865e07d
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-2.nix
@@ -0,0 +1,13 @@
+let {
+
+  as = {
+    x = 123;
+    y = 456;
+  };
+
+  bs = {
+    x = 789;
+    inherit (as) x;
+  };
+  
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-3.nix b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-3.nix
new file mode 100644
index 000000000000..114d19779f86
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-3.nix
@@ -0,0 +1,13 @@
+let {
+
+  as = {
+    x = 123;
+    y = 456;
+  };
+
+  bs = rec {
+    x = 789;
+    inherit (as) x;
+  };
+  
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-4.nix b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-4.nix
new file mode 100644
index 000000000000..77417432b347
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-4.nix
@@ -0,0 +1,4 @@
+{
+  services.ssh.port = 22;
+  services.ssh.port = 23;
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-7.nix b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-7.nix
new file mode 100644
index 000000000000..bbc3eb08c0f6
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-dup-attrs-7.nix
@@ -0,0 +1,9 @@
+rec {
+
+  x = 1;
+
+  as = {
+    inherit x;
+    inherit x;
+  };
+}
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-dup-formals.nix b/tvix/eval/src/tests/nix_tests/parse-fail-dup-formals.nix
new file mode 100644
index 000000000000..a0edd91a9666
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-dup-formals.nix
@@ -0,0 +1 @@
+{x, y, x}: x
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-eof-in-string.nix b/tvix/eval/src/tests/nix_tests/parse-fail-eof-in-string.nix
new file mode 100644
index 000000000000..19775d2ec810
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-eof-in-string.nix
@@ -0,0 +1,3 @@
+# https://github.com/NixOS/nix/issues/6562
+# Note that this file must not end with a newline.
+a 1"$
\ No newline at end of file
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs1.nix b/tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs1.nix
new file mode 100644
index 000000000000..11e40e66fd1b
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs1.nix
@@ -0,0 +1,4 @@
+{ 
+  x.z = 3; 
+  x = { y = 3; z = 3; }; 
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs2.nix b/tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs2.nix
new file mode 100644
index 000000000000..17da82e5f0c7
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-mixed-nested-attrs2.nix
@@ -0,0 +1,4 @@
+{ 
+  x.y.y = 3; 
+  x = { y.y= 3; z = 3; }; 
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-patterns-1.nix b/tvix/eval/src/tests/nix_tests/parse-fail-patterns-1.nix
new file mode 100644
index 000000000000..7b40616417b8
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-patterns-1.nix
@@ -0,0 +1 @@
+args@{args, x, y, z}: x
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-regression-20060610.nix b/tvix/eval/src/tests/nix_tests/parse-fail-regression-20060610.nix
new file mode 100644
index 000000000000..b1934f7e1e82
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-regression-20060610.nix
@@ -0,0 +1,11 @@
+let {
+  x =
+    {gcc}:
+    {
+      inherit gcc;
+    };
+
+  body = ({
+    inherit gcc;
+  }).gcc;
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-uft8.nix b/tvix/eval/src/tests/nix_tests/parse-fail-uft8.nix
new file mode 100644
index 000000000000..34948d48aed2
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-uft8.nix
@@ -0,0 +1 @@
+123 é 4
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-undef-var-2.nix b/tvix/eval/src/tests/nix_tests/parse-fail-undef-var-2.nix
new file mode 100644
index 000000000000..c10a52b1ea42
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-undef-var-2.nix
@@ -0,0 +1,7 @@
+let {
+
+  f = {x, y : ["baz" "bar" z "bat"]}: x + y;
+
+  body = f {x = "foo"; y = "bar";};
+
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-fail-undef-var.nix b/tvix/eval/src/tests/nix_tests/parse-fail-undef-var.nix
new file mode 100644
index 000000000000..7b63008110db
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-fail-undef-var.nix
@@ -0,0 +1 @@
+x: y
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-1.nix b/tvix/eval/src/tests/nix_tests/parse-okay-1.nix
new file mode 100644
index 000000000000..23a58ed109b1
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-1.nix
@@ -0,0 +1 @@
+{x, y, z}: x + y + z
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-crlf.nix b/tvix/eval/src/tests/nix_tests/parse-okay-crlf.nix
new file mode 100644
index 000000000000..21518d4c6d80
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-crlf.nix
@@ -0,0 +1,17 @@
+rec {
+
+  /* Dit is

+  een test. */
+
+  x = 
+  # Dit is een test.
y;
+  
+  y = 123;

+
+  # CR or CR/LF (but not explicit \r's) in strings should be
+  # translated to LF.
+  foo = "multi
line

+  string
+  test\r";
+
+  z = 456;
}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-5.nix b/tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-5.nix
new file mode 100644
index 000000000000..f4b9efd0c596
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-5.nix
@@ -0,0 +1,4 @@
+{
+  services.ssh = { enable = true; };
+  services.ssh.port = 23;
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-6.nix b/tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-6.nix
new file mode 100644
index 000000000000..ae6d7a769305
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-dup-attrs-6.nix
@@ -0,0 +1,4 @@
+{
+  services.ssh.port = 23;
+  services.ssh = { enable = true; };
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-1.nix b/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-1.nix
new file mode 100644
index 000000000000..fd1001c8cafc
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-1.nix
@@ -0,0 +1,4 @@
+{ 
+  x = { y = 3; z = 3; }; 
+  x.q = 3; 
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-2.nix b/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-2.nix
new file mode 100644
index 000000000000..ad066b680384
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-2.nix
@@ -0,0 +1,4 @@
+{ 
+  x.q = 3; 
+  x = { y = 3; z = 3; }; 
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-3.nix b/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-3.nix
new file mode 100644
index 000000000000..45a33e480373
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-mixed-nested-attrs-3.nix
@@ -0,0 +1,7 @@
+{
+    services.ssh.enable = true;
+    services.ssh = { port = 123; };
+    services = {
+        httpd.enable = true;
+    };
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-regression-20041027.nix b/tvix/eval/src/tests/nix_tests/parse-okay-regression-20041027.nix
new file mode 100644
index 000000000000..ae2e256eeaaa
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-regression-20041027.nix
@@ -0,0 +1,11 @@
+{stdenv, fetchurl /* pkgconfig, libX11 */ }:
+
+stdenv.mkDerivation {
+  name = "libXi-6.0.1";
+  src = fetchurl {
+    url = http://freedesktop.org/~xlibs/release/libXi-6.0.1.tar.bz2;
+    md5 = "7e935a42428d63a387b3c048be0f2756";
+  };
+/*  buildInputs = [pkgconfig];
+  propagatedBuildInputs = [libX11]; */
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-regression-751.nix b/tvix/eval/src/tests/nix_tests/parse-okay-regression-751.nix
new file mode 100644
index 000000000000..05c78b3016d3
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-regression-751.nix
@@ -0,0 +1,2 @@
+let const = a: "const"; in
+''${ const { x = "q"; }}''
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-subversion.nix b/tvix/eval/src/tests/nix_tests/parse-okay-subversion.nix
new file mode 100644
index 000000000000..356272815d26
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-subversion.nix
@@ -0,0 +1,43 @@
+{ localServer ? false
+, httpServer ? false
+, sslSupport ? false
+, pythonBindings ? false
+, javaSwigBindings ? false
+, javahlBindings ? false
+, stdenv, fetchurl
+, openssl ? null, httpd ? null, db4 ? null, expat, swig ? null, j2sdk ? null
+}:
+
+assert expat != null;
+assert localServer -> db4 != null;
+assert httpServer -> httpd != null && httpd.expat == expat;
+assert sslSupport -> openssl != null && (httpServer -> httpd.openssl == openssl);
+assert pythonBindings -> swig != null && swig.pythonSupport;
+assert javaSwigBindings -> swig != null && swig.javaSupport;
+assert javahlBindings -> j2sdk != null;
+
+stdenv.mkDerivation {
+  name = "subversion-1.1.1";
+
+  builder = /foo/bar;
+  src = fetchurl {
+    url = http://subversion.tigris.org/tarballs/subversion-1.1.1.tar.bz2;
+    md5 = "a180c3fe91680389c210c99def54d9e0";
+  };
+
+  # This is a hopefully temporary fix for the problem that
+  # libsvnjavahl.so isn't linked against libstdc++, which causes
+  # loading the library into the JVM to fail.
+  patches = if javahlBindings then [/javahl.patch] else [];
+
+  openssl = if sslSupport then openssl else null;
+  httpd = if httpServer then httpd else null;
+  db4 = if localServer then db4 else null;
+  swig = if pythonBindings || javaSwigBindings then swig else null;
+  python = if pythonBindings then swig.python else null;
+  j2sdk = if javaSwigBindings then swig.j2sdk else
+          if javahlBindings then j2sdk else null;
+
+  inherit expat localServer httpServer sslSupport
+          pythonBindings javaSwigBindings javahlBindings;
+}
diff --git a/tvix/eval/src/tests/nix_tests/parse-okay-url.nix b/tvix/eval/src/tests/nix_tests/parse-okay-url.nix
new file mode 100644
index 000000000000..08de27d0a4ce
--- /dev/null
+++ b/tvix/eval/src/tests/nix_tests/parse-okay-url.nix
@@ -0,0 +1,8 @@
+[ x:x
+  https://svn.cs.uu.nl:12443/repos/trace/trunk
+  http://www2.mplayerhq.hu/MPlayer/releases/fonts/font-arial-iso-8859-1.tar.bz2
+  http://losser.st-lab.cs.uu.nl/~armijn/.nix/gcc-3.3.4-static-nix.tar.gz
+  http://fpdownload.macromedia.com/get/shockwave/flash/english/linux/7.0r25/install_flash_player_7_linux.tar.gz
+  https://ftp5.gwdg.de/pub/linux/archlinux/extra/os/x86_64/unzip-6.0-14-x86_64.pkg.tar.zst
+  ftp://ftp.gtk.org/pub/gtk/v1.2/gtk+-1.2.10.tar.gz
+]
diff --git a/tvix/eval/src/tests/one_offs.rs b/tvix/eval/src/tests/one_offs.rs
new file mode 100644
index 000000000000..23bc9465d6bb
--- /dev/null
+++ b/tvix/eval/src/tests/one_offs.rs
@@ -0,0 +1,36 @@
+use crate::*;
+
+#[test]
+fn test_source_builtin() {
+    // Test an evaluation with a source-only builtin. The test ensures
+    // that the artificially constructed thunking is correct.
+
+    let mut eval = Evaluation::new_impure("builtins.testSourceBuiltin", None);
+    eval.src_builtins.push(("testSourceBuiltin", "42"));
+
+    let result = eval.evaluate();
+    assert!(
+        result.errors.is_empty(),
+        "evaluation failed: {:?}",
+        result.errors
+    );
+
+    let value = result.value.unwrap();
+    assert!(
+        matches!(value, Value::Integer(42)),
+        "expected the integer 42, but got {}",
+        value,
+    );
+}
+
+#[test]
+fn skip_broken_bytecode() {
+    let result = Evaluation::new(/* code = */ "x", None).evaluate();
+
+    assert_eq!(result.errors.len(), 1);
+
+    assert!(matches!(
+        result.errors[0].kind,
+        ErrorKind::UnknownStaticVariable
+    ));
+}
diff --git a/tvix/eval/src/tests/tvix_tests/README.md b/tvix/eval/src/tests/tvix_tests/README.md
new file mode 100644
index 000000000000..b493aa81f12f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/README.md
@@ -0,0 +1,19 @@
+These tests are "native" to Tvix and exist in addition to the Nix test
+suite.
+
+All of these are straightforward code snippets which are expected to
+produce a certain result.
+
+# `identity-*` tests
+
+Files named `identity-*.nix` contain code that is supposed to produce
+itself exactly after evaluation.
+
+These are useful for testing literals.
+
+# `eval-okay-*` tests
+
+Files named `eval-okay-*.nix` contain code which is supposed to
+evaluate to the output in the corresponding `eval-okay-*.exp` file.
+
+This convention is taken from the original Nix test suite.
diff --git a/tvix/eval/src/tests/tvix_tests/directory/default.nix b/tvix/eval/src/tests/tvix_tests/directory/default.nix
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/directory/default.nix
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-thunk-error.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-thunk-error.nix
new file mode 100644
index 000000000000..bb0d5920d757
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-thunk-error.nix
@@ -0,0 +1 @@
+builtins.genList (_: {}.foo) 1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-notcallable.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-notcallable.nix
new file mode 100644
index 000000000000..345b76fde037
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-notcallable.nix
@@ -0,0 +1,5 @@
+# attribute sets with a non-callable `__toString` can not be
+# serialised to JSON.
+builtins.toJSON {
+  __toString = 42;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-strong.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-strong.nix
new file mode 100644
index 000000000000..d1c72dc6783a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-builtins-tojson-tostring-strong.nix
@@ -0,0 +1,6 @@
+# String coercions when using builtins.toJSON on an attribute set with
+# a `__toString` attribute should be weak.
+builtins.toJSON {
+  __toString = self: self.x;
+  x = 42;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-closed-formals.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-closed-formals.nix
new file mode 100644
index 000000000000..937604c563e9
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-closed-formals.nix
@@ -0,0 +1 @@
+({x}: x) {x = 1; y = 2;}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-deep-forced-thunk-error.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-deep-forced-thunk-error.nix
new file mode 100644
index 000000000000..b7a758302266
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-deep-forced-thunk-error.nix
@@ -0,0 +1 @@
+[ (throw "error!") ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-deepseq.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-deepseq.nix
new file mode 100644
index 000000000000..9baa49b063ec
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-deepseq.nix
@@ -0,0 +1 @@
+builtins.deepSeq { x = abort "foo"; } 456
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-foldlStrict-strict-op-application.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-foldlStrict-strict-op-application.nix
new file mode 100644
index 000000000000..adc029b2f29e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-foldlStrict-strict-op-application.nix
@@ -0,0 +1,4 @@
+builtins.foldl'
+  (_: f: f null)
+  (throw "This doesn't explode")
+  [ (_: throw "Not the final value, but is still forced!") (_: 23) ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-force-before-value-pointer-equality.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-force-before-value-pointer-equality.nix
new file mode 100644
index 000000000000..de6c94325661
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-force-before-value-pointer-equality.nix
@@ -0,0 +1,5 @@
+let
+  x = throw "I have been forced";
+in
+
+x == x
\ No newline at end of file
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-getEnv-coercion.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-getEnv-coercion.nix
new file mode 100644
index 000000000000..fe48a5690c46
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-getEnv-coercion.nix
@@ -0,0 +1 @@
+builtins.getEnv { var = "PATH"; __toString = self: self.var; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-infinite-recursion.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-infinite-recursion.nix
new file mode 100644
index 000000000000..5e4fd3789cd6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-infinite-recursion.nix
@@ -0,0 +1 @@
+let x = x; in x
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-outer-value-never-pointer-equal.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-outer-value-never-pointer-equal.nix
new file mode 100644
index 000000000000..a8c3cedf61d7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-outer-value-never-pointer-equal.nix
@@ -0,0 +1,7 @@
+# For an explanation of this behavior see //tvix/docs/value-pointer-equality.md
+let
+  x = { foo = throw "foo"; };
+in
+
+# while `builtins.seq x null` would succeed, this fails!
+x == x
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-parsedrvname-coerce.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-parsedrvname-coerce.nix
new file mode 100644
index 000000000000..a1218de3fe58
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-parsedrvname-coerce.nix
@@ -0,0 +1 @@
+builtins.parseDrvName { outPath = "lol"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-remove.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-remove.nix
new file mode 100644
index 000000000000..539e0eb0a6f6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-remove.nix
@@ -0,0 +1,5 @@
+let {
+  attrs = {x = 123; y = 456;};
+
+  body = (removeAttrs attrs ["x"]).x;
+}
\ No newline at end of file
diff --git a/tvix/eval/src/tests/tvix_tests/eval-fail-seq.nix b/tvix/eval/src/tests/tvix_tests/eval-fail-seq.nix
new file mode 100644
index 000000000000..cddbbfd3261e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-fail-seq.nix
@@ -0,0 +1 @@
+builtins.seq (abort "foo") 2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.nix
new file mode 100644
index 000000000000..8e282d1280f1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-access-strange-identifier.nix
@@ -0,0 +1,8 @@
+let
+  # There is no syntax for accessing this identifier in an ordinary
+  # way.
+  "foo bar" = 42;
+in ({
+  # but we *can* inherit it back out
+  inherit "foo bar";
+})."foo bar"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.exp
new file mode 100644
index 000000000000..94ba9a881ae6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.exp
@@ -0,0 +1 @@
+[ /bin /binbar /binbar /binbar /binbar /bin/bar /bin/bin ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.nix
new file mode 100644
index 000000000000..462f670882a0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-add-paths.nix
@@ -0,0 +1,9 @@
+[
+  (/bin + "/")
+  (/bin + "bar")
+  (let name = "bar"; in /bin + name)
+  (let name = "bar"; in /bin + "${name}")
+  (let name = "bar"; in /bin + "/" + "${name}")
+  (let name = "bar"; in /bin + "/${name}")
+  (/bin + /bin)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.exp
new file mode 100644
index 000000000000..08ef6079f899
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.exp
@@ -0,0 +1 @@
+{ add = 37.34; div = 1.05714; mul = 105.154; sub = 14.35; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.nix
new file mode 100644
index 000000000000..9d12aee061b2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-float.nix
@@ -0,0 +1,6 @@
+{
+  add = 12.34 + 25.0;
+  sub = 20.05 - 5.7;
+  mul = 28.42 * 3.70;
+  div = 18.5 / 17.5;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.exp
new file mode 100644
index 000000000000..a5711e8bfe9b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.exp
@@ -0,0 +1 @@
+{ add = 20; div = 3; mul = 8; sub = 15; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.nix
new file mode 100644
index 000000000000..c53790db099b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-arithmetic-int.nix
@@ -0,0 +1,6 @@
+{
+  add = 15 + 5;
+  sub = 20 - 5;
+  mul = 4 * 2;
+  div = 9 / 3;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.exp
new file mode 100644
index 000000000000..aabe6ec3909c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.exp
@@ -0,0 +1 @@
+21
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.nix
new file mode 100644
index 000000000000..8934b3d8cf02
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-assert-thunk-condition.nix
@@ -0,0 +1,7 @@
+let
+  condition = x: y: x < y;
+in
+
+# The function application here will become a thunk which verifies that
+# assert forces the condition expression correctly.
+assert condition 21 42; 21
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.exp
new file mode 100644
index 000000000000..60d3b2f4a4cd
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.exp
@@ -0,0 +1 @@
+15
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.nix
new file mode 100644
index 000000000000..587aec893372
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-inherit-literal.nix
@@ -0,0 +1,2 @@
+# the 'from' part of an `inherit` can be any expression.
+{ inherit ({a = 15;}) a; }.a
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.exp
new file mode 100644
index 000000000000..a779fce51abc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.exp
@@ -0,0 +1 @@
+{ a = 1; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.nix
new file mode 100644
index 000000000000..68880bcfd857
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-simple-inherit.nix
@@ -0,0 +1,4 @@
+let
+  a = 1;
+in
+{ inherit a; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.exp
new file mode 100644
index 000000000000..fedf8f25a693
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.exp
@@ -0,0 +1 @@
+{ a = "ok"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.nix
new file mode 100644
index 000000000000..9596be22b831
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-lhs.nix
@@ -0,0 +1 @@
+{} // { a = "ok"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.exp
new file mode 100644
index 000000000000..fedf8f25a693
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.exp
@@ -0,0 +1 @@
+{ a = "ok"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.nix
new file mode 100644
index 000000000000..117c01141357
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-empty-rhs.nix
@@ -0,0 +1 @@
+{ a = "ok"; } // {}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.exp
new file mode 100644
index 000000000000..c2234a47e2b8
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.exp
@@ -0,0 +1 @@
+{ name = "foo"; other = 42; value = "bar"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.nix
new file mode 100644
index 000000000000..6f71684902e5
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update-kv-lhs.nix
@@ -0,0 +1 @@
+{ name = "foo"; value = "bar"; } // { other = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.exp
new file mode 100644
index 000000000000..57f4d541bd85
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.exp
@@ -0,0 +1 @@
+{ a = 15; b = "works"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.nix
new file mode 100644
index 000000000000..735602fe02d5
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-attrs-update.nix
@@ -0,0 +1 @@
+{ a = 15; } // { b = "works"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.exp
new file mode 100644
index 000000000000..60a773f4af69
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.exp
@@ -0,0 +1 @@
+[ "bar" "foo" "" "bar" "." "" "" "" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.nix
new file mode 100644
index 000000000000..bc59613f541d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-basenameof.nix
@@ -0,0 +1,10 @@
+[
+  (builtins.baseNameOf /foo/bar)
+  (builtins.baseNameOf "foo")
+  (builtins.baseNameOf "foo///")
+  (builtins.baseNameOf "foo/bar")
+  (builtins.baseNameOf "./.")
+  (builtins.baseNameOf "")
+  (builtins.baseNameOf /.)
+  (builtins.toString (builtins.baseNameOf /.))
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.exp
new file mode 100644
index 000000000000..c3ac813de6b9
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.exp
@@ -0,0 +1 @@
+[ 18 18.9 18.9 19.1 19 42 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.nix
new file mode 100644
index 000000000000..b04b1d1fa6ba
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-add.nix
@@ -0,0 +1,8 @@
+[
+  (builtins.add 7 11)
+  (builtins.add 7.9 11)
+  (builtins.add 7 11.9)
+  (builtins.add 7.2 11.9)
+  (builtins.add 7.1 11.9)
+  (builtins.add (builtins.add 21 10) 11)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.exp
new file mode 100644
index 000000000000..82ca7e6b6db7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.exp
@@ -0,0 +1 @@
+[ true true false false false false true true false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.nix
new file mode 100644
index 000000000000..12d62632dd6c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-all.nix
@@ -0,0 +1,15 @@
+[
+  (builtins.all (x: x) [ ])
+  (builtins.all (x: x) [ true true true ])
+  (builtins.all (x: x) [ false false false ])
+  (builtins.all (x: x) [ true true false ])
+  (builtins.all (x: x) [ false true true ])
+
+  # evaluation should short-circuit
+  (builtins.all (x: x) [ true false (builtins.abort "should be unreachable") ])
+
+  # arbitrary functions supported
+  (builtins.all (x: x * 2 == 42) [ ])
+  (builtins.all (x: x * 2 == 42) [ 21 21 21 ])
+  (builtins.all (x: x * 2 == 42) [ 1 2 3 ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.exp
new file mode 100644
index 000000000000..d6846ac3f756
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.exp
@@ -0,0 +1 @@
+[ false true false true true true false true false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.nix
new file mode 100644
index 000000000000..2c659f130b65
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-any.nix
@@ -0,0 +1,15 @@
+[
+  (builtins.any (x: x) [ ])
+  (builtins.any (x: x) [ true true true ])
+  (builtins.any (x: x) [ false false false ])
+  (builtins.any (x: x) [ true true false ])
+  (builtins.any (x: x) [ false true true ])
+
+  # evaluation should short-circuit
+  (builtins.any (x: x) [ false true (builtins.abort "should be unreachable") ])
+
+  # arbitrary functions supported
+  (builtins.any (x: x * 2 == 42) [ ])
+  (builtins.any (x: x * 2 == 42) [ 7 21 42 ])
+  (builtins.any (x: x * 2 == 42) [ 1 2 3 ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.exp
new file mode 100644
index 000000000000..6521066a8ea5
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.exp
@@ -0,0 +1 @@
+[ [ ] [ "bar" "baz" "foo" ] [ "Baz" "Foo" "bar" ] [ "Eric Idle" "Graham Chapman" "John Cleese" "Michael Palin" "Terry Gilliam" "Terry Jones" ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.nix
new file mode 100644
index 000000000000..67f7dcee5672
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrnames.nix
@@ -0,0 +1,13 @@
+[
+  (builtins.attrNames {})
+  (builtins.attrNames { foo = 1; bar = 2; baz = 3; })
+  (builtins.attrNames { Foo = 1; bar = 2; Baz = 3; })
+  (builtins.attrNames {
+    "Graham Chapman" = true;
+    "John Cleese" = true;
+    "Terry Gilliam" = true;
+    "Eric Idle" = true;
+    "Terry Jones" = true;
+    "Michael Palin" = true;
+  })
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.exp
new file mode 100644
index 000000000000..35c369772038
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.exp
@@ -0,0 +1 @@
+[ [ ] [ 2 3 1 ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.nix
new file mode 100644
index 000000000000..5eced31f0d2d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-attrvalues.nix
@@ -0,0 +1,4 @@
+[
+  (builtins.attrValues {})
+  (builtins.attrValues { foo = 1; bar = 2; baz = 3; })
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.exp
new file mode 100644
index 000000000000..30b348853e44
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.exp
@@ -0,0 +1 @@
+[ 0 0 0 1 8 8 8 8 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.nix
new file mode 100644
index 000000000000..af40005ed983
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitand.nix
@@ -0,0 +1,10 @@
+[
+  (builtins.bitAnd 0 0)
+  (builtins.bitAnd 0 1)
+  (builtins.bitAnd 1 0)
+  (builtins.bitAnd 1 1)
+  (builtins.bitAnd 8 8)
+  (builtins.bitAnd 8 (builtins.add 4 4))
+  (builtins.bitAnd (builtins.add 4 4) 8)
+  (builtins.bitAnd (builtins.add 4 4) (builtins.add 4 4))
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.exp
new file mode 100644
index 000000000000..2556b4183c77
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.exp
@@ -0,0 +1 @@
+[ 0 1 1 1 8 8 8 8 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.nix
new file mode 100644
index 000000000000..9c28f6d7acee
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitor.nix
@@ -0,0 +1,10 @@
+[
+  (builtins.bitOr 0 0)
+  (builtins.bitOr 1 0)
+  (builtins.bitOr 0 1)
+  (builtins.bitOr 1 1)
+  (builtins.bitOr 8 8)
+  (builtins.bitOr 8 (builtins.add 4 4))
+  (builtins.bitOr (builtins.add 4 4) 8)
+  (builtins.bitOr (builtins.add 4 4) (builtins.add 4 4))
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.exp
new file mode 100644
index 000000000000..457157d45970
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.exp
@@ -0,0 +1 @@
+[ 0 1 1 0 8 8 0 0 0 0 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.nix
new file mode 100644
index 000000000000..80e363fb0748
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-bitxor.nix
@@ -0,0 +1,12 @@
+[
+  (builtins.bitXor 0 0)
+  (builtins.bitXor 1 0)
+  (builtins.bitXor 0 1)
+  (builtins.bitXor 1 1)
+  (builtins.bitXor 8 0)
+  (builtins.bitXor 0 8)
+  (builtins.bitXor 8 8)
+  (builtins.bitXor 8 (builtins.add 4 4))
+  (builtins.bitXor (builtins.add 4 4) 8)
+  (builtins.bitXor (builtins.add 4 4) (builtins.add 4 4))
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.nix
new file mode 100644
index 000000000000..cfbcbbb7684d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-builtins.nix
@@ -0,0 +1 @@
+[ builtins ]  == [ builtins.builtins ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.exp
new file mode 100644
index 000000000000..f8c0b2de5fba
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.exp
@@ -0,0 +1 @@
+[ 21 "+" 21 "=" 42 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.nix
new file mode 100644
index 000000000000..edac76d4461a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-catAttrs.nix
@@ -0,0 +1,10 @@
+builtins.catAttrs "foo" [
+  { foo = 21; }
+  { bar = 23; foo = "+"; }
+  { }
+  { bar = 12; }
+  { foo = 21 + 0; }
+  { foo = "="; }
+  ({ bar = 13; } // { baz = 89; })
+  { foo = 42; bar = 33; }
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.exp
new file mode 100644
index 000000000000..e69498c3e1c3
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.exp
@@ -0,0 +1 @@
+[ 0 -1 -1 0 0 0 1 1 -1 1 -1 1 -1 -1 -1 -1 0 1 -1 -1 1 -1 -1 0 1 1 1 1 -1 -1 -1 -1 -1 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.nix
new file mode 100644
index 000000000000..cce554b4494a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-compareVersions.nix
@@ -0,0 +1,46 @@
+let
+  cmp = a: b:
+    let
+      ord1 = builtins.compareVersions a b;
+      ord2 = builtins.compareVersions b a;
+    in
+      assert ord1 == -ord2; ord1;
+in
+
+[
+  (cmp "1.2.3" "1.2.3")
+  (cmp "1.2.2" "1.2.3")
+  (cmp "1.2.3" "1.2.40")
+  (cmp "1.2.3" ".1.2.3")
+  (cmp "1.2.3" "1..2.3")
+  (cmp "1.2.3" "1.2.3.")
+  (cmp "1.2.3" "1.2")
+  (cmp "1.2.3" "1.2.a")
+  (cmp "1a.b" "1a.2")
+  (cmp "1" "")
+  (cmp "1.0" "1.0.0")
+  (cmp "2.3" "2.3pre")
+  (cmp "2.3" "2.3.0pre")
+  (cmp "2.3pre" "2.3.0pre")
+  (cmp "2.3" "2.3prepre")
+  (cmp "2.3pre" "2.3prepre")
+  (cmp "2.3prepre" "2.3prepre")
+  # check that the plain word comparison (via Ord) behaves the same
+  (cmp "foo" "bar")
+  (cmp "FoO" "fOo")
+  (cmp "foo" "fooo")
+  (cmp "foopre" "foo")
+  # Subset of test cases from eval-okay-versions.nix shipped by C++ Nix
+  (cmp "1.0" "2.3")
+  (cmp "2.1" "2.3")
+  (cmp "2.3" "2.3")
+  (cmp "2.5" "2.3")
+  (cmp "3.1" "2.3")
+  (cmp "2.3.1" "2.3")
+  (cmp "2.3.1" "2.3a")
+  (cmp "2.3pre1" "2.3")
+  (cmp "2.3pre3" "2.3pre12")
+  (cmp "2.3a" "2.3c")
+  (cmp "2.3pre1" "2.3c")
+  (cmp "2.3pre1" "2.3q")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.exp
new file mode 100644
index 000000000000..64ae529ac257
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.exp
@@ -0,0 +1 @@
+[ [ ] [ 1 2 3 4 5 6 ] [ [ 1 ] [ 2 ] [ 3 ] ] [ 1 2 3 4 5 6 ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.nix
new file mode 100644
index 000000000000..19ef5eba1145
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-concat-lists.nix
@@ -0,0 +1,6 @@
+[
+  (builtins.concatLists [ ])
+  (builtins.concatLists [ [ 1 2 ] [ 3 4 ] [ 5 6 ] ])
+  (builtins.concatLists [ [ [ 1 ] [ 2 ] ] [ [ 3 ] ] [ ] ])
+  (builtins.concatLists [ [ 1 2 ] [ ] [ 3 4 ] [ ] [ 5 6 ] ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.exp
new file mode 100644
index 000000000000..73e9bc33b083
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.exp
@@ -0,0 +1 @@
+[ 3 7 0 1 0 0.5 0.5 0.5 42 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.nix
new file mode 100644
index 000000000000..98b8b74bdf2b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-div.nix
@@ -0,0 +1,11 @@
+[
+  (builtins.div 9 3)
+  (builtins.div 7 1)
+  (builtins.div 3 9)
+  (builtins.div 4 4)
+  (builtins.div 1 2)
+  (builtins.div 1.0 2)
+  (builtins.div 1 2.0)
+  (builtins.div 1.0 2.0)
+  (builtins.div (builtins.div 84 4) 0.5)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.exp
new file mode 100644
index 000000000000..3701c9d75f94
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.exp
@@ -0,0 +1 @@
+[ "foo" "bar" "baz" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.nix
new file mode 100644
index 000000000000..762adeebbf0a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-elemat.nix
@@ -0,0 +1,5 @@
+[
+  (builtins.elemAt [ "foo" "bar" "baz" ] 0)
+  (builtins.elemAt [ "foo" "bar" "baz" ] 1)
+  (builtins.elemAt [ "foo" "bar" "baz" ] 2)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.exp
new file mode 100644
index 000000000000..fb94ebaa492c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.exp
@@ -0,0 +1 @@
+[ [ 1 2 3 4 5 ] [ ] [ 2 2 2 ] [ [ 1 2 ] [ 3 4 ] ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.nix
new file mode 100644
index 000000000000..b621fdb43e7c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-filter.nix
@@ -0,0 +1,13 @@
+[
+  (builtins.filter (_: true) [ 1 2 3 4 5 ])
+  (builtins.filter (_: false) [ 1 2 3 4 5 ])
+  (builtins.filter (x: x == 2) [ 1 2 1 2 1 2 ])
+
+  (builtins.filter (x: (builtins.length x) > 0) [
+    [ ]
+    [ 1 2 ]
+    [ ]
+    [ ]
+    [ 3 4 ]
+  ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.exp
new file mode 100644
index 000000000000..89fa6c681064
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.exp
@@ -0,0 +1 @@
+[ 1 2 3 { bar = { baz = 3; }; } ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.nix
new file mode 100644
index 000000000000..87a2adbcd3dd
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-getattr.nix
@@ -0,0 +1,6 @@
+[
+  (builtins.getAttr "foo" { foo = 1; bar = 2; baz = 3; })
+  (builtins.getAttr "bar" { foo = 1; bar = 2; baz = 3; })
+  (builtins.getAttr "baz" { foo = 1; bar = 2; baz = 3; })
+  (builtins.getAttr "foo" { foo = { bar = { baz = 3; }; }; })
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.exp
new file mode 100644
index 000000000000..94649819caab
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.exp
@@ -0,0 +1 @@
+{ fred = [ { x = "fred"; y = "fred"; } ]; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.nix
new file mode 100644
index 000000000000..d62ae628dc71
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-groupby-thunk.nix
@@ -0,0 +1,6 @@
+
+builtins.groupBy
+  (v: v.x)
+  [ (rec { y = x; x = "fred"; }) ]
+
+
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.exp
new file mode 100644
index 000000000000..541fe347cbcd
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.exp
@@ -0,0 +1 @@
+[ true true true false false true false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.nix
new file mode 100644
index 000000000000..e87e186b641d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-hasattr.nix
@@ -0,0 +1,9 @@
+[
+  (builtins.hasAttr "foo" { foo = 1; bar = 2; baz = 3; })
+  (builtins.hasAttr "bar" { foo = 1; bar = 2; baz = 3; })
+  (builtins.hasAttr "baz" { foo = 1; bar = 2; baz = 3; })
+  (builtins.hasAttr "FOO" { foo = 1; bar = 2; baz = 3; })
+  (builtins.hasAttr "foo" {})
+  (builtins.hasAttr ("f" + "o" + "o") { foo = 1; })
+  (builtins.hasAttr ("b" + "a" + "r") { foo = 1; })
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.exp
new file mode 100644
index 000000000000..afe288459f2e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.exp
@@ -0,0 +1 @@
+[ "foo" 1 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.nix
new file mode 100644
index 000000000000..1741a7aac4bb
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-head.nix
@@ -0,0 +1,4 @@
+[
+  (builtins.head [ "foo" ])
+  (builtins.head [ 1 2 3 ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.exp
new file mode 100644
index 000000000000..e80eb6ef1465
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.exp
@@ -0,0 +1 @@
+[ 0 1 3 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.nix
new file mode 100644
index 000000000000..ef1f638cbbdc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-length.nix
@@ -0,0 +1,5 @@
+[
+  (builtins.length [])
+  (builtins.length [ 1 ])
+  (builtins.length [ "one" "two" "three" ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.exp
new file mode 100644
index 000000000000..31f4598bb504
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.exp
@@ -0,0 +1 @@
+[ true true true true false false false false true true true true false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.nix
new file mode 100644
index 000000000000..cd2d0c209c3a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-lessThan.nix
@@ -0,0 +1,15 @@
+[
+  (builtins.lessThan 2 3)
+  (builtins.lessThan 2.0 3)
+  (builtins.lessThan 2 3.0)
+  (builtins.lessThan 2.0 3.0)
+  (builtins.lessThan 3 2)
+  (builtins.lessThan 3.0 2)
+  (builtins.lessThan 3 2.0)
+  (builtins.lessThan 3.0 2.0)
+  (builtins.lessThan 10 (builtins.add 9 2))
+  (builtins.lessThan (builtins.add 9 1) 11)
+  (builtins.lessThan (builtins.add 9 1) (builtins.add 9 2))
+  (builtins.lessThan "a" "b")
+  (builtins.lessThan "b" "a")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.exp
new file mode 100644
index 000000000000..6cf53040320f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.exp
@@ -0,0 +1 @@
+[ [ 1 2 3 4 5 ] [ 2 4 6 8 10 ] [ 2 4 6 8 10 ] [ 2 4 6 8 10 ] [ 1 2 3 4 5 ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.nix
new file mode 100644
index 000000000000..71b351fd55b0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-map.nix
@@ -0,0 +1,19 @@
+[
+  # identity function
+  (builtins.map (x: x) [ 1 2 3 4 5 ])
+
+  # double stuff
+  (builtins.map (x: x * 2) [ 1 2 3 4 5 ])
+
+  # same but with a closure this time
+  (
+    let n = 2;
+    in builtins.map (x: x * n) [ 1 2 3 4 5 ]
+  )
+
+  # same, but with a builtin
+  (builtins.map (builtins.mul 2) [ 1 2 3 4 5 ])
+
+  # from global scope
+  (map (x: x) [ 1 2 3 4 5 ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.exp
new file mode 100644
index 000000000000..e3e0f03a8af5
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.exp
@@ -0,0 +1 @@
+[ 36 0 0 14 42 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.nix
new file mode 100644
index 000000000000..2a8d6c4214c3
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-mul.nix
@@ -0,0 +1,7 @@
+[
+  (builtins.mul 4 9)
+  (builtins.mul 0 7)
+  (builtins.mul 7 0)
+  (builtins.mul 7 2)
+  (builtins.mul (builtins.mul 4 0.5) 21)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.exp
new file mode 100644
index 000000000000..d2390db4f528
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.exp
@@ -0,0 +1 @@
+[ { right = [ 1 2 3 4 5 ]; wrong = [ ]; } { right = [ ]; wrong = [ 1 2 3 4 5 ]; } { right = [ 2 ]; wrong = [ 1 3 4 5 ]; } { right = [ [ 1 2 ] [ 3 4 ] ]; wrong = [ [ 1 ] [ 2 ] [ 3 ] ]; } ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.nix
new file mode 100644
index 000000000000..0587330ff917
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-partition.nix
@@ -0,0 +1,13 @@
+[
+  (builtins.partition (_: true) [ 1 2 3 4 5])
+  (builtins.partition (_: false) [ 1 2 3 4 5])
+  (builtins.partition (x: x == 2) [ 1 2 3 4 5])
+
+  (builtins.partition (x: (builtins.length x) > 1) [
+    [ 1 ]
+    [ 1 2 ]
+    [ 2 ]
+    [ 3 ]
+    [ 3 4 ]
+  ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.exp
new file mode 100644
index 000000000000..c2cb89bac663
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.exp
@@ -0,0 +1 @@
+[ "fabir" "a" "1a1" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.nix
new file mode 100644
index 000000000000..b8101c448bb0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-replaceStrings.nix
@@ -0,0 +1,5 @@
+[
+  (builtins.replaceStrings ["oo" "a"] ["a" "i"] "foobar")
+  (builtins.replaceStrings ["o"] ["a"] "a")
+  (builtins.replaceStrings ["" ""] ["1" "2"] "a")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.exp
new file mode 100644
index 000000000000..222a0093f570
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.exp
@@ -0,0 +1 @@
+[ [ "1" "2" "3" ] [ "2" "3" "16" ] [ "22" "11" "pre" "408963" "823" "e" "2" "c" "9" "b" "0" "a" "0" ] [ "9" "4" "1" "rc" "1" ] [ "9" "4" "0" "20220721" ] [ "0" "1" "alpha" ] [ "unstable" "2022" "09" "20" ] [ "30" "pre" "9" ] [ "0" "pre+date=" "2021" "11" "30" ] [ "1" "2" "0" "_pre" "23" ] [ "0" "1" "0" "pre" "71" "_" "170" "f" "840" ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.nix
new file mode 100644
index 000000000000..4083e86714fc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-splitVersion.nix
@@ -0,0 +1,13 @@
+[
+  (builtins.splitVersion "1.2.3")
+  (builtins.splitVersion "2.3.16")
+  (builtins.splitVersion "22.11pre408963.823e2c9b0a0")
+  (builtins.splitVersion "9.4.1-rc1")
+  (builtins.splitVersion "9.4.0.20220721")
+  (builtins.splitVersion "0.1-alpha")
+  (builtins.splitVersion "unstable-2022-09-20")
+  (builtins.splitVersion "30.pre9")
+  (builtins.splitVersion "0.pre+date=2021-11-30")
+  (builtins.splitVersion "1.2.0_pre23")
+  (builtins.splitVersion "0.1.0pre71_170f840")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.exp
new file mode 100644
index 000000000000..b019be4bfd7a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.exp
@@ -0,0 +1 @@
+[ 3 "hello" 9 4 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.nix
new file mode 100644
index 000000000000..b7d51db3c578
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-string-length.nix
@@ -0,0 +1,10 @@
+[
+  (builtins.stringLength "foo")
+  (let s = "hello"; in (builtins.substring 0 (builtins.stringLength s) s))
+  (builtins.stringLength ("foo" + "${"bar" + "baz"}"))
+
+  # feel free to delete this test case at any time, it's just to show: This is a
+  # thing at the moment. We may want to break compatibility with this aspect of
+  # the C++ Nix implementation at any time.
+  (builtins.stringLength "😀")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.exp
new file mode 100644
index 000000000000..51842eccfac5
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.exp
@@ -0,0 +1 @@
+[ -4 -3.1 -4.9 -4.7 -4 42 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.nix
new file mode 100644
index 000000000000..2929c4dddd81
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-sub.nix
@@ -0,0 +1,8 @@
+[
+  (builtins.sub 7 11)
+  (builtins.sub 7.9 11)
+  (builtins.sub 7 11.9)
+  (builtins.sub 7.2 11.9)
+  (builtins.sub 7.9 11.9)
+  (builtins.sub (builtins.sub 123 23) 58)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.exp
new file mode 100644
index 000000000000..192548e94978
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.exp
@@ -0,0 +1 @@
+"42"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.nix
new file mode 100644
index 000000000000..626ae1d1be55
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring-coerce.nix
@@ -0,0 +1,5 @@
+# builtins.substring uses string coercion internally
+
+builtins.substring 0 2 {
+  __toString = _: "4200";
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.exp
new file mode 100644
index 000000000000..168276022898
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.exp
@@ -0,0 +1 @@
+[ "tes" "testing" "" "estin" "ting" "" "" "" "" "est" "est" "est" "est" "est" "est" "" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.nix
new file mode 100644
index 000000000000..f4ee82e2736f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-substring.nix
@@ -0,0 +1,18 @@
+[
+  (builtins.substring 0 3 "testing")
+  (builtins.substring 0 300 "testing")
+  (builtins.substring 3 0 "testing")
+  (builtins.substring 1 5 "testing")
+  (builtins.substring 3 5 "testing")
+  (builtins.substring 300 300 "testing")
+  (builtins.substring 301 300 "testing")
+  (builtins.substring 0 0 "")
+  (builtins.substring 0 1 "")
+  (builtins.substring (builtins.add 0 1) 3 "testing")
+  (builtins.substring 1 (builtins.add 3 0) "testing")
+  (builtins.substring (builtins.add 0 1) (builtins.add 3 0) "testing")
+  (builtins.substring (builtins.add 0 1) (builtins.add 3 0) "testing")
+  (builtins.substring (builtins.add 0 1) (builtins.add 3 0) ("test" + "ing"))
+  (builtins.substring (builtins.add 0 1) (builtins.add 3 0) ("test" + "ing"))
+  (builtins.substring 300 (-10) "testing")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.exp
new file mode 100644
index 000000000000..b9e3aa1ef79d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.exp
@@ -0,0 +1 @@
+[ [ ] [ 2 3 ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.nix
new file mode 100644
index 000000000000..2be9496a98e2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tail.nix
@@ -0,0 +1,4 @@
+[
+  (builtins.tail [ "foo" ])
+  (builtins.tail [ 1 2 3 ])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp
new file mode 100644
index 000000000000..3d4204d5a83e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.exp
@@ -0,0 +1 @@
+[ 2 [ "Hans" "James" "Joachim" ] 2 [ "Clawdia" "Mynheer" ] 981 3 2 2 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix
new file mode 100644
index 000000000000..d96ddb3bd16d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-thunked-function-calls.nix
@@ -0,0 +1,31 @@
+[
+  # This is independent of builtins
+  (builtins.length [ (builtins.throw "Ferge") (builtins.throw "Wehsal") ])
+  (builtins.attrNames {
+    Hans = throw "Castorp";
+    Joachim = throw "Ziemßen";
+    James = "Tienappel";
+  })
+
+  (builtins.length (builtins.map builtins.throw [ "Settembrini" "Naphta" ]))
+
+  (builtins.attrNames (builtins.mapAttrs builtins.throw {
+    Clawdia = "Chauchat";
+    Mynheer = "Peeperkorn";
+  }))
+
+  (builtins.length (builtins.genList (builtins.add "Marusja") 981))
+  (builtins.length (builtins.genList builtins.throw 3))
+
+  # These are hard to get wrong since the outer layer needs to be forced anyways
+  (builtins.length (builtins.genericClosure {
+    startSet = [
+      { key = 1; initial = true; }
+    ];
+    operator = { key, initial, ... }:
+      if initial
+      then [ { key = key - 1; initial = false;  value = throw "lol"; } ]
+      else [ ];
+  }))
+  (builtins.length (builtins.concatMap (m: [ m (builtins.throw m) ]) [ "Marusja" ]))
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.exp
new file mode 100644
index 000000000000..a148ebc3b53f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.exp
@@ -0,0 +1 @@
+[ "1" "4.200000" "" "" "1" "foo" "/etc" "Hello World" "Hello World" "1" "out" "2" "1 4.200000   1 foo /etc Hello World Hello World 1 out 2" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.nix
new file mode 100644
index 000000000000..e4dc18ac96a7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-toString.nix
@@ -0,0 +1,23 @@
+let
+  toStringableSet = {
+    __toString = self: self.content;
+    content = "Hello World";
+  };
+
+  toStringExamples = [
+    (toString 1)
+    (toString 4.2)
+    (toString null)
+    (toString false)
+    (toString true)
+    (toString "foo")
+    (toString /etc)
+    (toString toStringableSet)
+    (toString { __toString = _: toStringableSet; })
+    (toString { __toString = _: true; })
+    (toString { outPath = "out"; })
+    (toString { outPath = { outPath = { __toString = _: 2; }; }; })
+  ];
+in
+
+toStringExamples ++ [ (toString toStringExamples) ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.exp
new file mode 100644
index 000000000000..0a274c201fa8
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.exp
@@ -0,0 +1 @@
+"[42,\"hello\",13.37,[],[1,2,3],{},{\"name\":\"foo\",\"value\":42},{\"foo\":42}]"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.nix
new file mode 100644
index 000000000000..12e8c03b171d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-literals.nix
@@ -0,0 +1,11 @@
+# tests serialisation of literal data
+builtins.toJSON [
+  42
+  "hello"
+  13.37
+  [ ]
+  [ 1 2 3 ]
+  { }
+  { name = "foo"; value = 42; }
+  { foo = 42; }
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.exp
new file mode 100644
index 000000000000..69667de5a1c3
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.exp
@@ -0,0 +1 @@
+"{\"a\":40,\"b\":2}"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.nix
new file mode 100644
index 000000000000..70755c8c6dbe
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath-nested.nix
@@ -0,0 +1,8 @@
+# Attribute sets with an `outPath` can contain _any_ serialisable
+# value in that field.
+builtins.toJSON {
+  outPath = {
+    a = 40;
+    b = 2;
+  };
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.exp
new file mode 100644
index 000000000000..82dd08179843
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.exp
@@ -0,0 +1 @@
+"\"/nix/store/jzka5ndnygkkfjfvpqwjipqp75lhz138-emacs-28.2\""
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.nix
new file mode 100644
index 000000000000..7f9d95ac60f4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-outpath.nix
@@ -0,0 +1,5 @@
+# Attribute sets with an `outPath` have that outPath itself serialised
+# to string.
+builtins.toJSON {
+  outPath = "/nix/store/jzka5ndnygkkfjfvpqwjipqp75lhz138-emacs-28.2";
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.exp
new file mode 100644
index 000000000000..9ccd94224ba8
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.exp
@@ -0,0 +1 @@
+"[42,42,\"42\"]"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.nix
new file mode 100644
index 000000000000..16234ab4514a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-thunks.nix
@@ -0,0 +1,9 @@
+let
+  a = b * 2;
+  b = 21;
+in
+builtins.toJSON [
+  a
+  ((n: n * 2) 21)
+  (builtins.toJSON a)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.exp
new file mode 100644
index 000000000000..2661fd257bf9
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.exp
@@ -0,0 +1 @@
+"\"it's 42\""
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.nix
new file mode 100644
index 000000000000..ec6f8d947cec
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-tojson-tostring.nix
@@ -0,0 +1,8 @@
+# Attribute sets with a `__toString` attribute JSON-serialise with a
+# string coercion of the function call result.
+
+builtins.toJSON {
+  __toString = self: "it's " + (builtins.toString (self.x * self.y));
+  x = 21;
+  y = 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.exp
new file mode 100644
index 000000000000..1ea054fc2d72
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.exp
@@ -0,0 +1 @@
+[ "null" "bool" "bool" "int" "int" "float" "string" "string" "set" "set" "list" "lambda" "path" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.nix
new file mode 100644
index 000000000000..a3cb659ecfde
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-of.nix
@@ -0,0 +1,22 @@
+let
+  fix = f: let x = f x; in x;
+in
+
+fix (self:
+  [
+    (builtins.typeOf null)
+    (builtins.typeOf true)
+    (builtins.typeOf (true && false))
+    (builtins.typeOf 12)
+    (builtins.typeOf (builtins.add 21 21))
+    (builtins.typeOf 1.2)
+    (builtins.typeOf "foo")
+    (builtins.typeOf "${"foo" + "bar"}baz")
+    (builtins.typeOf {})
+    # (builtins.typeOf { foo.bar = 32; }.foo) # TODO: re-enable when nested keys are done
+    (builtins.typeOf ({ name = "foo"; value = 13; } // { name = "bar"; }))
+    (builtins.typeOf self)
+    (builtins.typeOf fix)
+    (builtins.typeOf /nix/store)
+  ]
+)
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.exp
new file mode 100644
index 000000000000..724c1f9c34b9
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.exp
@@ -0,0 +1 @@
+[ true true false true true false true true false true true false true true false true true false true true false true true false true true true false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.nix
new file mode 100644
index 000000000000..3d688cb8bd68
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-builtins-type-predicates.nix
@@ -0,0 +1,34 @@
+let
+  # apply is thunked, so we can create a thunked value using the identity function
+  thunk = x: x;
+in
+[
+  (builtins.isAttrs { bar = throw "baz"; })
+  (builtins.isAttrs (thunk { foo = 13; }))
+  (builtins.isAttrs (thunk 123))
+  (builtins.isBool true)
+  (builtins.isBool (thunk false))
+  (builtins.isBool (thunk "lol"))
+  (builtins.isFloat 1.2)
+  (builtins.isFloat (thunk (1 * 1.0)))
+  (builtins.isFloat 1)
+  (builtins.isFunction thunk)
+  (builtins.isFunction (thunk thunk))
+  (builtins.isFunction {})
+  (builtins.isInt 1)
+  (builtins.isInt (thunk 42))
+  (builtins.isInt 1.0)
+  (builtins.isList [ (throw "oh no") (abort "it's over") ])
+  (builtins.isList (thunk [ 21 21 ]))
+  (builtins.isList (thunk {}))
+  (builtins.isNull null)
+  (builtins.isNull (thunk null))
+  (builtins.isNull 42)
+  (builtins.isPath ./relative)
+  (builtins.isPath (thunk /absolute))
+  (builtins.isPath "/not/a/path")
+  (builtins.isString "simple")
+  (builtins.isString "${{ outPath = "coerced"; }}")
+  (builtins.isString "hello ${"interpolation"}")
+  (builtins.isString true)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-ceil.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-ceil.exp
new file mode 100644
index 000000000000..dffbbe59f0a0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-ceil.exp
@@ -0,0 +1 @@
+[ 4 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-ceil.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-ceil.nix
new file mode 100644
index 000000000000..5835bf829b03
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-ceil.nix
@@ -0,0 +1 @@
+[ (builtins.ceil 3.4) ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.exp
new file mode 100644
index 000000000000..c3bb809c9ffb
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.exp
@@ -0,0 +1 @@
+[ false false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.nix
new file mode 100644
index 000000000000..7c4333668df8
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-pointer-compare.nix
@@ -0,0 +1,14 @@
+# For an explanation of this behavior see //tvix/docs/value-pointer-equality.md
+let
+  g = x:
+    owo: "th" + x;
+in
+[
+  (
+    { q = g "ia"; } == { q = g ("i"+"a"); }
+  )
+
+  (
+    [ (g "ia") ] == [ (g ("i"+"a")) ]
+  )
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.exp
new file mode 100644
index 000000000000..be54b4b4e39e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.exp
@@ -0,0 +1 @@
+"done"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.nix
new file mode 100644
index 000000000000..bda364f42992
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-self.nix
@@ -0,0 +1,4 @@
+let
+  # self-recursive function should be able to close over itself
+  f = n: if n <= 0 then "done" else f (n - 1);
+in f 10
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.nix
new file mode 100644
index 000000000000..305463775217
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-closure-with-shadowing.nix
@@ -0,0 +1,14 @@
+# If a closure closes over a variable that is statically known *and*
+# available dynamically through `with`, the statically known one must
+# have precedence.
+
+let
+  # introduce statically known `a` (this should be the result)
+  a = 1;
+in
+
+# introduce some closure depth to force both kinds of upvalue
+# resolution, and introduce a dynamically known `a` within the
+# closures
+let f = b: with { a = 2; }; c: a + b + c;
+in f 0 0
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.exp
new file mode 100644
index 000000000000..95a0e7378b5e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.exp
@@ -0,0 +1 @@
+{ eq = false; ge = false; gt = false; le = false; lt = false; ne = false; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.nix
new file mode 100644
index 000000000000..2b511f56eecb
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-false.nix
@@ -0,0 +1,8 @@
+{
+  eq = 6.9 == 4.2;
+  ne = 4.2 != 4.2;
+  lt = 2.5 < 1.5;
+  le = 2.5 <= 1.5;
+  gt = 1.5 > 2.5;
+  ge = 1.5 >= 2.5;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.exp
new file mode 100644
index 000000000000..9160829dde78
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.exp
@@ -0,0 +1 @@
+{ eq = true; ge = true; gt = true; le = true; lt = true; ne = true; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.nix
new file mode 100644
index 000000000000..c505a85b1f3c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-float-true.nix
@@ -0,0 +1,8 @@
+{
+  eq = 4.2 == 4.2;
+  ne = 6.9 != 4.2;
+  lt = 1.5 < 2.5;
+  le = 2.5 <= 2.5;
+  gt = 2.3 > 1.2;
+  ge = 2.3 >= 2.3;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.exp
new file mode 100644
index 000000000000..95a0e7378b5e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.exp
@@ -0,0 +1 @@
+{ eq = false; ge = false; gt = false; le = false; lt = false; ne = false; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.nix
new file mode 100644
index 000000000000..7d6b30419fac
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-false.nix
@@ -0,0 +1,8 @@
+{
+  eq = 69 == 42;
+  ne = 42 != 42;
+  lt = 2 < 1;
+  le = 2 <= 1;
+  gt = 1 > 2;
+  ge = 1 >= 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.exp
new file mode 100644
index 000000000000..9160829dde78
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.exp
@@ -0,0 +1 @@
+{ eq = true; ge = true; gt = true; le = true; lt = true; ne = true; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.nix
new file mode 100644
index 000000000000..0bf474e53fd0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-int-true.nix
@@ -0,0 +1,8 @@
+{
+  eq = 42 == 42;
+  ne = 69 != 42;
+  lt = 1 < 2;
+  le = 2 <= 2;
+  gt = 2 > 1;
+  ge = 2 >= 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.exp
new file mode 100644
index 000000000000..95a0e7378b5e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.exp
@@ -0,0 +1 @@
+{ eq = false; ge = false; gt = false; le = false; lt = false; ne = false; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.nix
new file mode 100644
index 000000000000..61b206c03376
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-false.nix
@@ -0,0 +1,8 @@
+{
+  eq = 6.9 == 4;
+  ne = 4.0 != 4;
+  lt = 2.5 < 1;
+  le = 2 <= 1.5;
+  gt = 1 > 1.1;
+  ge = 1.5 >= 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.exp
new file mode 100644
index 000000000000..9160829dde78
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.exp
@@ -0,0 +1 @@
+{ eq = true; ge = true; gt = true; le = true; lt = true; ne = true; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.nix
new file mode 100644
index 000000000000..ad77074710ca
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-num-true.nix
@@ -0,0 +1,8 @@
+{
+  eq = 42.0 == 42;
+  ne = 6.9 != 4;
+  lt = 1.5 < 2;
+  le = 2.0 <= 2.0;
+  gt = 1.1 > 1;
+  ge = 2.3 >= 2.3;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.exp
new file mode 100644
index 000000000000..95a0e7378b5e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.exp
@@ -0,0 +1 @@
+{ eq = false; ge = false; gt = false; le = false; lt = false; ne = false; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.nix
new file mode 100644
index 000000000000..b5773a21d374
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-false.nix
@@ -0,0 +1,8 @@
+{
+  eq = "test" == "not test";
+  ne = "test" != "test";
+  lt = "bcd" < "abc";
+  le = "bcd" <= "abc";
+  gt = "abc" > "bcd";
+  ge = "abc" >= "bcd";
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.exp
new file mode 100644
index 000000000000..9160829dde78
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.exp
@@ -0,0 +1 @@
+{ eq = true; ge = true; gt = true; le = true; lt = true; ne = true; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.nix
new file mode 100644
index 000000000000..172d2237e9e2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-cmp-str-true.nix
@@ -0,0 +1,8 @@
+{
+  eq = "test" == "test";
+  ne = "test" != "not test";
+  lt = "abc" < "bcd";
+  le = "bcd" <= "bcd";
+  gt = "bcd" > "abc";
+  ge = "bcd" >= "bcd";
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.exp
new file mode 100644
index 000000000000..d874518a37ed
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.exp
@@ -0,0 +1 @@
+[ "lordnikon" "zerocool" /tmp/31337h4x0r "fooblah" "blahfoo" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.nix
new file mode 100644
index 000000000000..6ddd30e7a189
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-coerce-opadd.nix
@@ -0,0 +1,7 @@
+[
+  ({ __toString = _: "lord"; } + "nikon")
+  ("zero" + { __toString = _: "cool"; })
+  (/tmp/31337 + "h4x0r")
+  ("foo" + { outPath="blah"; })
+  ({ outPath="blah"; } + "foo")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.exp
new file mode 100644
index 000000000000..3b7fd398198a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.exp
@@ -0,0 +1 @@
+[ false true true true false true false false false true false false false true true ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.nix
new file mode 100644
index 000000000000..9b73df61d84e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-compare-lists.nix
@@ -0,0 +1,17 @@
+[
+  ([1 2] < [1])
+  ([1 2] < [2 3])
+  ([1 2] < [2])
+  ([1 2] < [1 2 3])
+  ([3 4] < [1])
+  ([1 2] > [1])
+  ([1 2] > [2 3])
+  ([1 2] > [2])
+  ([1 2] > [1 2 3])
+  ([3 4] > [1])
+  ([1 2] <= [1])
+  ([1 2] >= [2 3])
+  ([1 2] >= [2])
+  ([1 2] <= [1 2 3])
+  ([3 4] >= [1])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.exp
new file mode 100644
index 000000000000..3bed31f76e3f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.exp
@@ -0,0 +1 @@
+[ 1 2 3 4 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.nix
new file mode 100644
index 000000000000..de332cd29f3a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-lists.nix
@@ -0,0 +1 @@
+[ 1 2 ] ++ [ 3 4 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.exp
new file mode 100644
index 000000000000..cd4bc1ab64cc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.exp
@@ -0,0 +1 @@
+"hello world"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.nix
new file mode 100644
index 000000000000..1fc708929989
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concat-strings.nix
@@ -0,0 +1 @@
+"hello " + "world"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.exp
new file mode 100644
index 000000000000..14d804aa22f5
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.exp
@@ -0,0 +1 @@
+[ "a" "z" "b" "z" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.nix
new file mode 100644
index 000000000000..149a0722b11d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concatmap.nix
@@ -0,0 +1 @@
+(builtins.concatMap (x: [x] ++ ["z"]) ["a" "b"])
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.exp
new file mode 100644
index 000000000000..93987647ffe6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.exp
@@ -0,0 +1 @@
+[ "" "foobarxyzzy" "foo, bar, xyzzy" "foo" "" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.nix
new file mode 100644
index 000000000000..adc4c41bd551
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-concatstringssep.nix
@@ -0,0 +1,8 @@
+with builtins;
+
+[ (concatStringsSep "" [])
+  (concatStringsSep "" ["foo" "bar" "xyzzy"])
+  (concatStringsSep ", " ["foo" "bar" "xyzzy"])
+  (concatStringsSep ", " ["foo"])
+  (concatStringsSep ", " [])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.exp
new file mode 100644
index 000000000000..c508d5366f70
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.exp
@@ -0,0 +1 @@
+false
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.nix
new file mode 100644
index 000000000000..361ba9144594
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-nested-non-set.nix
@@ -0,0 +1,3 @@
+# ? operator should work even if encountering a non-set value on the
+# walk
+{ a.b = 42; } ? a.b.c
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.exp
new file mode 100644
index 000000000000..ca00e3c049d6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.exp
@@ -0,0 +1 @@
+[ false false false false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.nix
new file mode 100644
index 000000000000..c086759f456b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-contains-non-set.nix
@@ -0,0 +1,3 @@
+# Nix allows using the ? operator on non-set types, in which case it
+# should always return false.
+[ (123 ? key) ("foo" ? key) (null ? key) ([ "key" ] ? key) ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.exp
new file mode 100644
index 000000000000..7cf54d959611
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.exp
@@ -0,0 +1 @@
+{ a = { b = { c = { d = { e = { f = { g = "deep!"; }; }; }; }; }; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.nix
new file mode 100644
index 000000000000..91649d0c6dd4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-attrs.nix
@@ -0,0 +1 @@
+{ a.b.c.d.e.f.g = "deep!"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.exp
new file mode 100644
index 000000000000..3bed31f76e3f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.exp
@@ -0,0 +1 @@
+[ 1 2 3 4 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.nix
new file mode 100644
index 000000000000..7f13f1f27030
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with-closure.nix
@@ -0,0 +1,18 @@
+# This convoluted test constructs a situation in which dynamically
+# resolved upvalues refer `with` blocks introduced at different lambda
+# context boundaries, i.e. the access to a, b in the innermost closure
+# must be threaded through upvalues in several levels.
+
+(_:
+with { a = 1; b = 1; };
+
+_:
+with { b = 2; c = 2; };
+
+_:
+with { c = 3; d = 3; };
+
+_:
+with { d = 4; };
+
+[ a b c d ]) null null null null
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.exp
new file mode 100644
index 000000000000..3bed31f76e3f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.exp
@@ -0,0 +1 @@
+[ 1 2 3 4 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.nix
new file mode 100644
index 000000000000..7f1128b6707b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deeply-nested-with.nix
@@ -0,0 +1,6 @@
+with { a = 1; b = 1; };
+with { b = 2; c = 2; };
+with { c = 3; d = 3; };
+with { d = 4; };
+
+[ a b c d ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.exp
new file mode 100644
index 000000000000..8d38505c1686
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.exp
@@ -0,0 +1 @@
+456
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.nix
new file mode 100644
index 000000000000..53aa4b1dc251
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deepseq.nix
@@ -0,0 +1 @@
+builtins.deepSeq (let as = { x = 123; y = as; }; in as) 456
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.exp
new file mode 100644
index 000000000000..5993db7ccc5a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.exp
@@ -0,0 +1 @@
+[ false -2 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.nix
new file mode 100644
index 000000000000..1fbb3e853af4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-unary-formals.nix
@@ -0,0 +1,6 @@
+# Application of unary operators on deferred formals arguments (via
+# defaulting), see also b/255.
+[
+  (({ b ? !a, a }: b) { a = true; })
+  (({ b ? -a, a }: b) { a = 2; })
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.nix
new file mode 100644
index 000000000000..af227ae28e87
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-deferred-with.nix
@@ -0,0 +1,8 @@
+# Tests using `with` on a set that does not yet exist on the stack.
+
+let
+  result = with set; value;
+  set = {
+    value = 42;
+  };
+in result
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-dirof.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-dirof.exp
new file mode 100644
index 000000000000..ff464e4c30d4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-dirof.exp
@@ -0,0 +1 @@
+[ /foo "." "foo//" "foo" "." "." / "/" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-dirof.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-dirof.nix
new file mode 100644
index 000000000000..13cf47320515
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-dirof.nix
@@ -0,0 +1,10 @@
+[
+  (builtins.dirOf /foo/bar)
+  (builtins.dirOf "foo")
+  (builtins.dirOf "foo///")
+  (builtins.dirOf "foo/bar")
+  (builtins.dirOf "./.")
+  (builtins.dirOf "")
+  (builtins.dirOf /.)
+  (builtins.toString (builtins.dirOf /.))
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-elem.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-elem.exp
new file mode 100644
index 000000000000..3cf6c0e962f0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-elem.exp
@@ -0,0 +1 @@
+[ true false 30 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-elem.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-elem.nix
new file mode 100644
index 000000000000..71ea7a4ed03d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-elem.nix
@@ -0,0 +1,6 @@
+with import ./lib.nix;
+
+let xs = range 10 40; in
+
+[ (builtins.elem 23 xs) (builtins.elem 42 xs) (builtins.elemAt xs 20) ]
+
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.exp
new file mode 100644
index 000000000000..ffcd4415b08f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.exp
@@ -0,0 +1 @@
+{ }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.nix
new file mode 100644
index 000000000000..a1181431deca
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-empty-rec-inherit.nix
@@ -0,0 +1 @@
+rec { inherit; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.nix
new file mode 100644
index 000000000000..398f4a9dfc55
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-float.nix
@@ -0,0 +1 @@
+4.2 == 4.2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.nix
new file mode 100644
index 000000000000..dc52ba112a60
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-int.nix
@@ -0,0 +1 @@
+42 == 42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.nix
new file mode 100644
index 000000000000..5dbcb515295b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-eq-nested-list.nix
@@ -0,0 +1 @@
+[["f" ""]] == [["f" ""]]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.exp
new file mode 100644
index 000000000000..aa98a082a8ac
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.exp
@@ -0,0 +1 @@
+{ "3" = 3; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.nix
new file mode 100644
index 000000000000..aa98a082a8ac
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-escapify-integer-keys.nix
@@ -0,0 +1 @@
+{ "3" = 3; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fib.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-fib.exp
new file mode 100644
index 000000000000..8643cf6debac
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fib.exp
@@ -0,0 +1 @@
+89
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fib.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-fib.nix
new file mode 100644
index 000000000000..9a22d85ac5f1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fib.nix
@@ -0,0 +1,7 @@
+let
+  fib' = i: n: m: if i == 0
+    then n
+    else fib' (i - 1) m (n + m);
+
+  fib = n: fib' n 1 1;
+in fib 10
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fix.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-fix.exp
new file mode 100644
index 000000000000..c1581543511f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fix.exp
@@ -0,0 +1 @@
+{ a = 1; b = 21; c = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fix.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-fix.nix
new file mode 100644
index 000000000000..27d2fae1042e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fix.nix
@@ -0,0 +1,7 @@
+let
+  fix = f: let x = f x; in x;
+in fix(self: {
+  a = 1;
+  b = self.a + 20;
+  c = self.b * 2;
+})
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.exp
new file mode 100644
index 000000000000..c55d2be717f1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.exp
@@ -0,0 +1 @@
+1.23457
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.nix
new file mode 100644
index 000000000000..447bd5af7f5c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-float-repr.nix
@@ -0,0 +1,2 @@
+# Floats are displayed with a maximum of 5 digits
+1.23456789
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-floor.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-floor.exp
new file mode 100644
index 000000000000..6f98a7f48f2f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-floor.exp
@@ -0,0 +1 @@
+[ 3 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-floor.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-floor.nix
new file mode 100644
index 000000000000..c6b79c91a1b4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-floor.nix
@@ -0,0 +1 @@
+[ (builtins.floor 3.4) ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.nix
new file mode 100644
index 000000000000..fc4129a2543a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-elements.nix
@@ -0,0 +1,8 @@
+let
+  lst = builtins.foldl'
+    (acc: x: acc ++ [ x ])
+    [ ]
+    [ 42 (throw "this shouldn't be evaluated") ];
+in
+
+builtins.head lst
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.nix
new file mode 100644
index 000000000000..59fd29b55237
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict-lazy-initial-accumulator.nix
@@ -0,0 +1,4 @@
+builtins.foldl'
+  (_: x: x)
+  (throw "This is never forced")
+  [ "but the results of applying op are" 42 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.exp
new file mode 100644
index 000000000000..8d683a20fab7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.exp
@@ -0,0 +1 @@
+[ 6 [ 0 1 2 3 ] 2 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.nix
new file mode 100644
index 000000000000..44c0349387ff
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-foldlStrict.nix
@@ -0,0 +1,5 @@
+[
+  (builtins.foldl' builtins.add 0 [1 2 3])
+  (builtins.foldl' (l1: l2: l1 ++ l2) [0] [[1] [2 3]])
+  (builtins.foldl' (x: y: if x == 0 then y else x * y) 0 [1 2])
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp
new file mode 100644
index 000000000000..add5505a8287
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp
@@ -0,0 +1 @@
+"quote \" reverse solidus \\ solidus / backspace  formfeed  newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace  1 char unicode encoded e with accent é 2 char unicode encoded s with caron š 3 char unicode encoded rightwards arrow →"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix
new file mode 100644
index 000000000000..f00713507732
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix
@@ -0,0 +1,3 @@
+# This string contains all supported escapes in a JSON string, per json.org
+# \b and \f are not supported by Nix
+builtins.fromJSON ''"quote \" reverse solidus \\ solidus \/ backspace \b formfeed \f newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace \u0008 1 char unicode encoded e with accent \u00e9 2 char unicode encoded s with caron \u0161 3 char unicode encoded rightwards arrow \u2192"''
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp
new file mode 100644
index 000000000000..24aa21d78ff7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp
@@ -0,0 +1 @@
+[ { Image = { Animated = false; Height = 600; IDs = [ 116 943 234 38793 true false null -100 ]; Latitude = 37.7668; Longitude = -122.396; Thumbnail = { Height = 125; Url = "http://www.example.com/image/481989943"; Width = 100; }; Title = "View from 15th Floor"; Width = 800; }; } { name = "a"; value = "b"; } [ 1 2 3 4 ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix
new file mode 100644
index 000000000000..e530789446ef
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix
@@ -0,0 +1,24 @@
+[
+# RFC 7159, section 13.
+  (builtins.fromJSON
+    ''
+    {
+      "Image": {
+          "Width":  800,
+          "Height": 600,
+          "Title":  "View from 15th Floor",
+          "Thumbnail": {
+              "Url":    "http://www.example.com/image/481989943",
+              "Height": 125,
+              "Width":  100
+          },
+          "Animated" : false,
+          "IDs": [116, 943, 234, 38793, true  ,false,null, -100],
+          "Latitude":  37.7668,
+          "Longitude": -122.396
+      }
+    }
+  '')
+  (builtins.fromJSON ''{"name": "a", "value": "b"}'')
+  (builtins.fromJSON "[ 1, 2, 3, 4 ]")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.exp
new file mode 100644
index 000000000000..c1c9f8ffaf69
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.exp
@@ -0,0 +1 @@
+[ "stdenv" "fetchurl" "aterm-stdenv" "aterm-stdenv2" "libX11" "libXv" "mplayer-stdenv2.libXv-libX11" "mplayer-stdenv2.libXv-libX11_2" "nix-stdenv-aterm-stdenv" "nix-stdenv2-aterm2-stdenv2" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.nix
new file mode 100644
index 000000000000..cd95b971f8a9
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-functionargs.nix
@@ -0,0 +1,80 @@
+let
+
+  stdenvFun = { }: { name = "stdenv"; };
+  stdenv2Fun = { }: { name = "stdenv2"; };
+  fetchurlFun = { stdenv }: assert stdenv.name == "stdenv"; { name = "fetchurl"; };
+  atermFun = { stdenv, fetchurl }: { name = "aterm-${stdenv.name}"; };
+  aterm2Fun = { stdenv, fetchurl }: { name = "aterm2-${stdenv.name}"; };
+  nixFun = { stdenv, fetchurl, aterm }: { name = "nix-${stdenv.name}-${aterm.name}"; };
+
+  mplayerFun =
+    { stdenv, fetchurl, enableX11 ? false, xorg ? null, enableFoo ? true, foo ? null  }:
+    assert stdenv.name == "stdenv2";
+    assert enableX11 -> xorg.libXv.name == "libXv";
+    assert enableFoo -> foo != null;
+    { name = "mplayer-${stdenv.name}.${xorg.libXv.name}-${xorg.libX11.name}"; };
+
+  makeOverridable = f: origArgs: f origArgs //
+    { override = newArgs:
+        makeOverridable f (origArgs // (if builtins.isFunction newArgs then newArgs origArgs else newArgs));
+    };
+
+  callPackage_ = pkgs: f: args:
+    makeOverridable f ((builtins.intersectAttrs (builtins.functionArgs f) pkgs) // args);
+
+  allPackages =
+    { overrides ? (pkgs: pkgsPrev: { }) }:
+    let
+      callPackage = callPackage_ pkgs;
+      pkgs = pkgsStd // (overrides pkgs pkgsStd);
+      pkgsStd = {
+        inherit pkgs;
+        stdenv = callPackage stdenvFun { };
+        stdenv2 = callPackage stdenv2Fun { };
+        fetchurl = callPackage fetchurlFun { };
+        aterm = callPackage atermFun { };
+        xorg = callPackage xorgFun { };
+        mplayer = callPackage mplayerFun { stdenv = pkgs.stdenv2; enableFoo = false; };
+        nix = callPackage nixFun { };
+      };
+    in pkgs;
+
+  libX11Fun = { stdenv, fetchurl }: { name = "libX11"; };
+  libX11_2Fun = { stdenv, fetchurl }: { name = "libX11_2"; };
+  libXvFun = { stdenv, fetchurl, libX11 }: { name = "libXv"; };
+
+  xorgFun =
+    { pkgs }:
+    let callPackage = callPackage_ (pkgs // pkgs.xorg); in
+    {
+      libX11 = callPackage libX11Fun { };
+      libXv = callPackage libXvFun { };
+    };
+
+in
+
+let
+
+  pkgs = allPackages { };
+
+  pkgs2 = allPackages {
+    overrides = pkgs: pkgsPrev: {
+      stdenv = pkgs.stdenv2;
+      nix = pkgsPrev.nix.override { aterm = aterm2Fun { inherit (pkgs) stdenv fetchurl; }; };
+      xorg = pkgsPrev.xorg // { libX11 = libX11_2Fun { inherit (pkgs) stdenv fetchurl; }; };
+    };
+  };
+
+in
+
+  [ pkgs.stdenv.name
+    pkgs.fetchurl.name
+    pkgs.aterm.name
+    pkgs2.aterm.name
+    pkgs.xorg.libX11.name
+    pkgs.xorg.libXv.name
+    pkgs.mplayer.name
+    pkgs2.mplayer.name
+    pkgs.nix.name
+    pkgs2.nix.name
+  ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.nix
new file mode 100644
index 000000000000..80ae345d836b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-functor-call.nix
@@ -0,0 +1 @@
+{ x = 21; __functor = self: y: self.x * y; } 2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-genlist.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-genlist.exp
new file mode 100644
index 000000000000..cd4ca34f14df
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-genlist.exp
@@ -0,0 +1 @@
+[ 0 1 4 9 16 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-genlist.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-genlist.nix
new file mode 100644
index 000000000000..2c4dfba20324
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-genlist.nix
@@ -0,0 +1 @@
+builtins.genList (x: x * x) 5
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.exp
new file mode 100644
index 000000000000..074f5f07f5f0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.exp
@@ -0,0 +1 @@
+{ "'quoted'" = false; "-20°" = false; "2normal" = false; "45 44 43-'3 2 1" = false; "9front" = false; Very2Normal = true; VeryNormal = true; _'12 = true; "_'12.5" = false; __internal = true; _internal = true; abort = true; assert = true; "attr.path" = false; false = true; foldl' = true; normal = true; normal2 = true; null = true; or = true; throw = true; true = true; x = true; x' = true; x'' = true; "😀" = false; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.nix
new file mode 100644
index 000000000000..8f9aa2823801
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-identifier-formatting.nix
@@ -0,0 +1,30 @@
+{
+  __internal = true;
+  _internal = true;
+  normal = true;
+  VeryNormal = true;
+  normal2 = true;
+  Very2Normal = true;
+  _'12 = true;
+  foldl' = true;
+  x = true;
+  x' = true;
+  x'' = true;
+
+  true = true;
+  false = true;
+  null = true;
+  or = true;
+  "assert" = true; # -ish
+  throw = true;
+  abort = true;
+
+  "9front" = false;
+  "2normal" = false;
+  "-20°" = false;
+  "45 44 43-'3 2 1" = false;
+  "attr.path" = false;
+  "'quoted'" = false;
+  "_'12.5" = false;
+  "😀" = false;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-import.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-import.exp
new file mode 100644
index 000000000000..5ba7f64d78a7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-import.exp
@@ -0,0 +1 @@
+[ 42 42 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-import.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-import.nix
new file mode 100644
index 000000000000..49cd244f06c1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-import.nix
@@ -0,0 +1,4 @@
+[
+  (import ./directory)
+  (import ./directory/default.nix)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.nix
new file mode 100644
index 000000000000..dde81e5a7c30
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-inherit-string-ident.nix
@@ -0,0 +1,7 @@
+# identifiers in inherits can be string-like expressions
+
+let
+  set = {
+    inherit ({ value = 42; }) "value";
+  };
+in set.value
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.nix
new file mode 100644
index 000000000000..5c6702120fc4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals-deferred.nix
@@ -0,0 +1,3 @@
+# Tests formals which have internal default values that must be deferred.
+
+({ optional ? defaultValue, defaultValue }: optional) { defaultValue = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.nix
new file mode 100644
index 000000000000..c6dd5e9d54cf
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-internal-formals.nix
@@ -0,0 +1,3 @@
+# Tests formals which have internal default values.
+
+({ defaultValue, optional ? defaultValue }: optional) { defaultValue = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.exp
new file mode 100644
index 000000000000..25001b211fb8
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.exp
@@ -0,0 +1 @@
+{ a = 100; b = 200; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.nix
new file mode 100644
index 000000000000..3534132ed48b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-intersectattrs.nix
@@ -0,0 +1,3 @@
+builtins.intersectAttrs
+  { a = 1; b = 2; c = 3; }
+  { a = 100; b = 200; d = 5; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.nix
new file mode 100644
index 000000000000..f2ee49df8092
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lambda-identity.nix
@@ -0,0 +1,2 @@
+# Identity function is the simplest possible function.
+(x: x) 42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.nix
new file mode 100644
index 000000000000..dae170b06bad
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding-closure.nix
@@ -0,0 +1,4 @@
+let
+  f = n: n + a;
+  a = 2;
+in f 40
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.nix
new file mode 100644
index 000000000000..41c4c53ea271
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-late-binding.nix
@@ -0,0 +1,4 @@
+let
+  a = b;
+  b = 42;
+in a
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.exp
new file mode 100644
index 000000000000..48082f72f087
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.exp
@@ -0,0 +1 @@
+12
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.nix
new file mode 100644
index 000000000000..5a36964976ca
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-assert.nix
@@ -0,0 +1,8 @@
+assert true;
+
+let
+  x = assert false; 13;
+  y = 12;
+in
+
+{ inherit x y; }.y
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.exp
new file mode 100644
index 000000000000..1c70d1bcf188
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.exp
@@ -0,0 +1 @@
+[ true true false true true ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.nix
new file mode 100644
index 000000000000..d19d1213d695
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-equality.nix
@@ -0,0 +1,15 @@
+let
+  attrs1 = { x = 1 + 2; };
+  attrs2 = { x = 2 + 1; };
+  list1 = [ (1 + 2) ];
+  list2 = [ (2 + 1) ];
+  list3 = [ (2 + 2) ];
+  list4 = [ (2 + 2) ];
+  list5 = [ (2 + 2) ];
+in [
+  (attrs1 == attrs2)
+  (list1 == list2)
+  (list3 == list2)
+  (list4 == [ 4 ])
+  ([ 4 ] == list5)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.nix
new file mode 100644
index 000000000000..22ac14b3f19e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with-nested.nix
@@ -0,0 +1,5 @@
+# The 'namespace' of a with should only be evaluated if an identifier
+# from it is actually accessed.
+
+with (abort "should not be evaluated");
+let a = dynamic; in 42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.nix
new file mode 100644
index 000000000000..8b1a0191dcc0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-lazy-with.nix
@@ -0,0 +1,6 @@
+# The 'namespace' of a with should only be evaluated if an identifier
+# from it is actually accessed.
+
+with (abort "should not be evaluated");
+
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.exp
new file mode 100644
index 000000000000..5d2955ffd5e1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.exp
@@ -0,0 +1 @@
+{ one = 42; two = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.nix
new file mode 100644
index 000000000000..87fcffadee5f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-fix.nix
@@ -0,0 +1,9 @@
+let {
+  a = 21;
+  b = body.one;
+
+  body = {
+    one = a * 2;
+    two = b;
+  };
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.nix
new file mode 100644
index 000000000000..a5b05426689e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let-in-with.nix
@@ -0,0 +1 @@
+with {}; let { body = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.nix
new file mode 100644
index 000000000000..850e0252c2b6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-legacy-let.nix
@@ -0,0 +1,4 @@
+let {
+  a = 21;
+  body = a * 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.exp
new file mode 100644
index 000000000000..5776134d0e41
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.exp
@@ -0,0 +1 @@
+[ 1 2 3 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.nix
new file mode 100644
index 000000000000..b6c37c34f949
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-identifiers.nix
@@ -0,0 +1,5 @@
+let
+  a = 1;
+  "b" = 2;
+  ${"c"} = 3;
+in [ a b c ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.exp
new file mode 100644
index 000000000000..409940768f2a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.exp
@@ -0,0 +1 @@
+23
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.nix
new file mode 100644
index 000000000000..21196f48bcbe
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit-from-later-bound.nix
@@ -0,0 +1,13 @@
+let
+  inherit (c) d;
+  inherit (a) b c;
+
+  a = {
+    b = 20;
+    c = {
+      d = 3;
+    };
+  };
+in
+
+b + d
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.exp
new file mode 100644
index 000000000000..0cfbf08886fc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.exp
@@ -0,0 +1 @@
+2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.nix
new file mode 100644
index 000000000000..4ec270e3bf43
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-inherit.nix
@@ -0,0 +1,12 @@
+let
+  set = {
+    a = 1;
+  };
+in
+  let
+    set2 = {
+      b = 1;
+    };
+    inherit (set) a;
+    inherit (set2) b;
+  in a + b
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.exp
new file mode 100644
index 000000000000..00750edc07d6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.exp
@@ -0,0 +1 @@
+3
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.nix
new file mode 100644
index 000000000000..7a65a5b1cc9f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-sibling-access.nix
@@ -0,0 +1,5 @@
+let
+  a = 1;
+  b = 2;
+  c = a + b;
+in c
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.exp
new file mode 100644
index 000000000000..3bed31f76e3f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.exp
@@ -0,0 +1 @@
+[ 1 2 3 4 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.nix
new file mode 100644
index 000000000000..30981099cbde
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit-mixed.nix
@@ -0,0 +1,20 @@
+# This test mixes different ways of creating bindings in a let … in expression
+# to make sure that the compiler initialises the locals in the same order as
+# they are declared.
+
+let
+  d = 4;
+in
+
+# Trick to allow useless inherits in the following let
+with { _unused = null; };
+
+let
+  set = { b = 2; };
+  a = 1;
+  inherit (set) b;
+  c = 3;
+  inherit d;
+in
+
+[ a b c d ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.nix
new file mode 100644
index 000000000000..3d1c46b10bb2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-let-useful-plain-inherit.nix
@@ -0,0 +1,9 @@
+with { a = 1; };
+
+let
+  inherit a;
+in
+
+with { a = 2; };
+
+a
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.exp
new file mode 100644
index 000000000000..74abef7bc6ed
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.exp
@@ -0,0 +1 @@
+"AAbar"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.nix
new file mode 100644
index 000000000000..89888fd56178
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-listtoattrs.nix
@@ -0,0 +1,15 @@
+with builtins;
+let
+  fold = op: nul: list:
+    if list == []
+    then nul
+    else op (head list) (fold op nul (tail list));
+  concat =
+    fold (x: y: x + y) "";
+  asi = name: value : { inherit name value; };
+  list = [ ( asi "a" "A" ) ( asi "b" "B" ) ];
+  a = builtins.listToAttrs list;
+  b = builtins.listToAttrs ( list ++ list );
+  r = builtins.listToAttrs [ (asi "result" [ a b ]) ( asi "throw" (throw "this should not be thrown")) ];
+  x = builtins.listToAttrs [ (asi "foo" "bar") (asi "foo" "bla") ];
+in concat (map (x: x.a) r.result) + x.foo
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.nix
new file mode 100644
index 000000000000..8e1256d764e3
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-manual-rec.nix
@@ -0,0 +1,9 @@
+# Manual desugaring of something similar to `rec`, to test lower level
+# recursion primitives.
+
+let
+  set = with set; {
+    a = 21;
+    b = a * 2;
+  };
+in set.b
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.exp
new file mode 100644
index 000000000000..911ab51de5ca
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.exp
@@ -0,0 +1 @@
+{ set = { a = 1; b = 2; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.nix
new file mode 100644
index 000000000000..78b28909a29c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-attrs.nix
@@ -0,0 +1,9 @@
+{
+  set = {
+    a = 1;
+  };
+
+  set = {
+    b = 2;
+  };
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.exp
new file mode 100644
index 000000000000..768eaae61cfc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.exp
@@ -0,0 +1 @@
+{ set = { a = 21; b = 42; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.nix
new file mode 100644
index 000000000000..cea4cb1b4f0d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-merge-nested-rec-attrs.nix
@@ -0,0 +1,12 @@
+{
+  set = rec {
+    a = 21;
+  };
+
+  set = {
+    # Fun fact: This might be the only case in Nix where a lexical
+    # resolution of an identifier can only be resolved by looking at
+    # *siblings* in the AST.
+    b = 2 * a;
+  };
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.exp
new file mode 100644
index 000000000000..9839e480b781
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.exp
@@ -0,0 +1 @@
+"hello\nworld"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.nix
new file mode 100644
index 000000000000..84beb22ed5c3
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-multiline-string.nix
@@ -0,0 +1,2 @@
+''hello
+world''
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.exp
new file mode 100644
index 000000000000..b5c707cf462e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.exp
@@ -0,0 +1 @@
+{ a = { b = 15; }; b = { c = "test"; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.nix
new file mode 100644
index 000000000000..5d611930ca7a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-multiple-nested-attrs.nix
@@ -0,0 +1 @@
+{ a.b = 15; b.c = "test"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.exp
new file mode 100644
index 000000000000..edca9baca9c0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.exp
@@ -0,0 +1 @@
+{ a = 1; b = 2; c = 3; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.nix
new file mode 100644
index 000000000000..1b3feda432ef
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-mutually-recursive-let-binding.nix
@@ -0,0 +1,14 @@
+let
+  a = {
+    a = 3;
+    b = b.b;
+  };
+
+  b = {
+    a = a.a - 2;
+    b = 2;
+    c = a.c or 3;
+  };
+in
+
+a // b
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.nix
new file mode 100644
index 000000000000..e06b571a28bf
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-int.nix
@@ -0,0 +1 @@
+42 != 69
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.nix
new file mode 100644
index 000000000000..a83471e500ce
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-ne-string.nix
@@ -0,0 +1 @@
+"this" != "that"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.exp
new file mode 100644
index 000000000000..b6a7d89c68e0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.exp
@@ -0,0 +1 @@
+16
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.nix
new file mode 100644
index 000000000000..97bff7f07758
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-closure.nix
@@ -0,0 +1 @@
+(a: b: c: d: a + b + c + d) 1 3 5 7
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.exp
new file mode 100644
index 000000000000..209e3ef4b624
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.exp
@@ -0,0 +1 @@
+20
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.nix
new file mode 100644
index 000000000000..358925e992c7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-deferred-upvalue.nix
@@ -0,0 +1,6 @@
+let
+  doubler = n: outer n;
+  outer = let inner = n: a * n;
+            a = 2;
+          in inner;
+in doubler 10
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.exp
new file mode 100644
index 000000000000..d2c1c04da3c4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.exp
@@ -0,0 +1 @@
+[ true true true true true true true false false false false false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.nix
new file mode 100644
index 000000000000..47dcec7a95f4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-has-attrs.nix
@@ -0,0 +1,26 @@
+let
+  set = {
+    a.b.c = 123;
+    foo = {
+      bar = 23;
+    };
+    baz = 1;
+  };
+
+  tes = "random value";
+in
+
+[
+  (set ? a)
+  (set ? a.b)
+  (set ? a.b.c)
+  (set ? foo)
+  (set ? foo.bar)
+  (set.foo ? bar)
+  (set ? baz)
+  (set ? x)
+  (set ? x.y.z)
+  (tes ? bar)
+  (tes ? x.y.z)
+  (null ? null)
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.exp
new file mode 100644
index 000000000000..6db47b033efb
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.exp
@@ -0,0 +1 @@
+{ a = { b = 42; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.nix
new file mode 100644
index 000000000000..c75b7130e1ba
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-let.nix
@@ -0,0 +1,4 @@
+let
+  inner = 21;
+  set.a.b = inner * 2;
+in set
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.exp
new file mode 100644
index 000000000000..77eb325dde19
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.exp
@@ -0,0 +1 @@
+{ a = { b = { c = 42; }; }; outer = 21; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.nix
new file mode 100644
index 000000000000..797d11108ff4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-keys-rec.nix
@@ -0,0 +1,4 @@
+rec {
+  outer = 21;
+  a.b.c = outer * 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.exp
new file mode 100644
index 000000000000..e45ef1da2f54
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.exp
@@ -0,0 +1 @@
+[ 1 2 3 4 5 6 7 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.nix
new file mode 100644
index 000000000000..811bb4361bf7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let-slots.nix
@@ -0,0 +1,17 @@
+# This test deals with a tricky edge-case around scopes, where the
+# stack slot accounting must correctly account for the position at
+# which the body of a let expression is being initialised when
+# resolving upvalues.
+
+let
+  a = 1;
+  b = 2;
+  outer = let
+    c = 3;
+    d = 4;
+    inner = let
+      e = 5;
+      f = 6;
+    in g: [ a b c d e f g ];
+  in inner;
+in outer 7
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.exp
new file mode 100644
index 000000000000..7f8f011eb73d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.exp
@@ -0,0 +1 @@
+7
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.nix
new file mode 100644
index 000000000000..caaa20211928
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-let.nix
@@ -0,0 +1,7 @@
+let
+  a = let
+    b = 1;
+    c = 2;
+  in b + c;
+  b = 4;
+in a + b
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.nix
new file mode 100644
index 000000000000..8d0280bb8973
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-poisoning.nix
@@ -0,0 +1,4 @@
+let
+  null = 1;
+  f = n: n + null;
+in f 41
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.nix
new file mode 100644
index 000000000000..f3ad8293540f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-set-thunks.nix
@@ -0,0 +1,5 @@
+({
+  x = {
+    y = 42;
+  };
+}).x.y
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.exp
new file mode 100644
index 000000000000..d757cae1f588
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.exp
@@ -0,0 +1 @@
+{ outer = 42; sibling = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.nix
new file mode 100644
index 000000000000..31111d80810c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-siblings.nix
@@ -0,0 +1,7 @@
+rec {
+  outer =
+    let inner = sibling;
+    in inner;
+
+  sibling = 42;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.nix
new file mode 100644
index 000000000000..133929dd1961
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-thunks.nix
@@ -0,0 +1,7 @@
+# If a thunk yields another thunk, OpForce should keep forcing until
+# there is a value.
+let
+  a = b;
+  b = c;
+  c = 42;
+in a
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.exp
new file mode 100644
index 000000000000..0cfbf08886fc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.exp
@@ -0,0 +1 @@
+2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.nix
new file mode 100644
index 000000000000..ea5ef568edab
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nested-with.nix
@@ -0,0 +1,4 @@
+let
+  set1 = { a = 1; };
+  set2 = { a = 2; };
+in with set1; with set2; a
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.exp
new file mode 100644
index 000000000000..3a2e3f4984a0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.exp
@@ -0,0 +1 @@
+-1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.nix
new file mode 100644
index 000000000000..6f353056127c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-nix-version-cmp.nix
@@ -0,0 +1,5 @@
+# nixpkgs checks against the `builtins.nixVersion` and fails if it
+# doesn't like what it sees. To work around this we have a "user-agent
+# style" version (see cl/6858) that ensures compatibility.
+
+builtins.compareVersions "2.3" builtins.nixVersion
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.exp
new file mode 100644
index 000000000000..bbb332a5eea0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.exp
@@ -0,0 +1 @@
+[ "x" "y" ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.nix
new file mode 100644
index 000000000000..684c88f80067
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-observe-infinite-attrs.nix
@@ -0,0 +1,4 @@
+# The below attribute set is infinitely large, but we should be able
+# to observe it as long as we don't access its entire value.
+
+let as = { x = 123; y = as; }; in builtins.attrNames as.y.y
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.exp
new file mode 100644
index 000000000000..9d9185fcd155
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.exp
@@ -0,0 +1 @@
+[ true true false false true true false false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.nix
new file mode 100644
index 000000000000..650d7f028df2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-optimised-bools.nix
@@ -0,0 +1,21 @@
+let
+  makeTrue = _: true;
+  makeFalse = _: false;
+in
+[
+  # useless `false`
+  (false || makeTrue null) # true
+  (makeTrue null || false) # true
+
+  # useless `true`
+  (true && makeFalse null) # false
+  (makeFalse null && true) # false
+
+  # useless `||`
+  (true || makeFalse null) # true
+  (makeFalse null || true) # true
+
+  # useless `&&`
+  (false && makeTrue null) # false
+  (makeTrue null && false) # false
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.nix
new file mode 100644
index 000000000000..444f270af637
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-default.nix
@@ -0,0 +1 @@
+{ b = 1; }.b or 2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.exp
new file mode 100644
index 000000000000..0cfbf08886fc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.exp
@@ -0,0 +1 @@
+2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.nix
new file mode 100644
index 000000000000..ceffd0697b28
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested-default.nix
@@ -0,0 +1 @@
+{ a.b = 1; }.a.c or 2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.nix
new file mode 100644
index 000000000000..1a76594546b3
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-nested.nix
@@ -0,0 +1 @@
+{ a.b = 1; }.a.b or 2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.exp
new file mode 100644
index 000000000000..a833e32892e4
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.exp
@@ -0,0 +1 @@
+"works fine"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.nix
new file mode 100644
index 000000000000..fd09bfee64c2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator-non-set.nix
@@ -0,0 +1,2 @@
+# `or` operator should keep working if it encounters a non-set type.
+{ a.b = 42; }.a.b.c or "works fine"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.nix
new file mode 100644
index 000000000000..ce1e6e67c289
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-or-operator.nix
@@ -0,0 +1 @@
+{ a = 1; }.a or 2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.exp
new file mode 100644
index 000000000000..2483a2718301
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.exp
@@ -0,0 +1 @@
+{ a = { b = 15; c = "test"; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.nix
new file mode 100644
index 000000000000..4154ff9da29f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-overlapping-nested-attrs.nix
@@ -0,0 +1,4 @@
+{
+  a.b = 15;
+  a.c = "test";
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.nix
new file mode 100644
index 000000000000..fea6e234dcfd
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-parsedrvname.nix
@@ -0,0 +1,12 @@
+
+# the first dash followed by a non-alphabetic character separates
+# the "name" from the "version"
+
+assert builtins.parseDrvName "ripgrep-1.2"       == { name = "ripgrep";       version = "1.2"; };
+assert builtins.parseDrvName "rip-grep-1.2"      == { name = "rip-grep";      version = "1.2"; };
+assert builtins.parseDrvName "7zip_archiver-0.2" == { name = "7zip_archiver"; version = "0.2"; };
+assert builtins.parseDrvName "gcc-1-2"           == { name = "gcc";           version = "1-2"; };
+assert builtins.parseDrvName "bash--1-2"         == { name = "bash";          version = "-1-2"; };
+assert builtins.parseDrvName "xvidtune-?1-2"     == { name = "xvidtune";      version = "?1-2"; };
+
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.nix
new file mode 100644
index 000000000000..ab3d0369401c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-pathexists.nix
@@ -0,0 +1,2 @@
+builtins.pathExists ./lib.nix
+&& !builtins.pathExists ./bla.nix
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.exp
new file mode 100644
index 000000000000..5776134d0e41
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.exp
@@ -0,0 +1 @@
+[ 1 2 3 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.nix
new file mode 100644
index 000000000000..81f03d9e2b09
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-poisoned-scopes.nix
@@ -0,0 +1,6 @@
+let
+  true = 1;
+  false = 2;
+  null = 3;
+in
+[ true false null ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-readDir.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-readDir.exp
new file mode 100644
index 000000000000..bf8d2c14ea4f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-readDir.exp
@@ -0,0 +1 @@
+{ bar = "regular"; foo = "directory"; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-readDir.nix.disabled b/tvix/eval/src/tests/tvix_tests/eval-okay-readDir.nix.disabled
new file mode 100644
index 000000000000..a7ec9292aae2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-readDir.nix.disabled
@@ -0,0 +1 @@
+builtins.readDir ./readDir
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-readfile.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-readfile.exp
new file mode 100644
index 000000000000..a2c87d0c439f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-readfile.exp
@@ -0,0 +1 @@
+"builtins.readFile ./eval-okay-readfile.nix\n"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-readfile.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-readfile.nix
new file mode 100644
index 000000000000..82f7cb17435a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-readfile.nix
@@ -0,0 +1 @@
+builtins.readFile ./eval-okay-readfile.nix
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.exp
new file mode 100644
index 000000000000..ac8d062a6911
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.exp
@@ -0,0 +1 @@
+{ barbaz = 42; foobar = 42; val = 21; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.nix
new file mode 100644
index 000000000000..8d7a8cef8e10
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-dynamic-keys.nix
@@ -0,0 +1,5 @@
+rec {
+  val = 21;
+  ${"foo" + "bar"} = 42;
+  ${"bar" + "baz"} = val * 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.exp
new file mode 100644
index 000000000000..a1dca9bb6860
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.exp
@@ -0,0 +1 @@
+{ a = { b = 1; c = 2; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.nix
new file mode 100644
index 000000000000..7d037c6b37bc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-rec-nested-access.nix
@@ -0,0 +1,4 @@
+rec {
+  a.b = 1;
+  a.c = a.b * 2;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.nix
new file mode 100644
index 000000000000..a234705b5e53
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-recursive-attrs-all-features.nix
@@ -0,0 +1,13 @@
+let a = 1;
+in
+(rec {
+  inherit a;
+
+  b = {
+    c = a + 20;
+  };
+
+  inherit (b) c;
+
+  d = c * 2;
+}).d
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.exp
new file mode 100644
index 000000000000..950103539123
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.exp
@@ -0,0 +1 @@
+[ true true false true true true true false false true false [ "foobar" ] [ "FOO" ] [ "/path/to/" "/path/to" "foobar" "nix" ] [ null null "foobar" "cc" ] ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.nix
new file mode 100644
index 000000000000..f774e00a215a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-regex-match.nix
@@ -0,0 +1,29 @@
+with builtins;
+
+let
+
+  matches = pat: s: match pat s != null;
+
+  splitFN = match "((.*)/)?([^/]*)\\.(nix|cc)";
+
+in
+
+[
+  (matches "foobar" "foobar")
+  (matches "fo*" "f")
+  (matches "fo+" "f")
+  (matches "fo*" "fo")
+  (matches "fo*" "foo")
+  (matches "fo+" "foo")
+  (matches "fo{1,2}" "foo")
+  (matches "fo{1,2}" "fooo")
+  (matches "fo*" "foobar")
+  (matches "[[:space:]]+([^[:space:]]+)[[:space:]]+" "  foo   ")
+  (matches "[[:space:]]+([[:upper:]]+)[[:space:]]+" "  foo   ")
+
+  (match "(.*)\\.nix" "foobar.nix")
+  (match "[[:space:]]+([[:upper:]]+)[[:space:]]+" "  FOO   ")
+
+  (splitFN "/path/to/foobar.nix")
+  (splitFN "foobar.cc")
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-remove.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-remove.exp
new file mode 100644
index 000000000000..8d38505c1686
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-remove.exp
@@ -0,0 +1 @@
+456
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-remove.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-remove.nix
new file mode 100644
index 000000000000..4ad5ba897fa7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-remove.nix
@@ -0,0 +1,5 @@
+let {
+  attrs = {x = 123; y = 456;};
+
+  body = (removeAttrs attrs ["x"]).y;
+}
\ No newline at end of file
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.exp
new file mode 100644
index 000000000000..b4a1e66d6b8a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.exp
@@ -0,0 +1 @@
+[ 1 2 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.nix
new file mode 100644
index 000000000000..2f719dcef5be
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-repeated-list-to-attrs.nix
@@ -0,0 +1,13 @@
+# Ensure that builtins.listToAttrs returns the first instance of a key.
+
+let
+  inherit (builtins) foldl' listToAttrs;
+
+  input = [ { name = "result"; value = 1; } { name = "result"; value = 2; } ];
+
+  # foldl-based version of listToAttrs with the _opposite_ behaviour.
+  listToAttrs' = list: foldl' ( acc: elem: acc // { ${elem.name} = elem.value; }) {} list;
+in [
+  (listToAttrs input).result
+  (listToAttrs' input).result
+]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-seq.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-seq.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-seq.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-seq.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-seq.nix
new file mode 100644
index 000000000000..fd0806c199a5
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-seq.nix
@@ -0,0 +1 @@
+(builtins.seq 1 2) + (builtins.seq [ (throw "list") ] 20) + (builtins.seq { boing = throw "set"; } 20)
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.exp
new file mode 100644
index 000000000000..7f8f011eb73d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.exp
@@ -0,0 +1 @@
+7
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.nix
new file mode 100644
index 000000000000..56445454fec7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-closure.nix
@@ -0,0 +1 @@
+(a: b: a + b) 2 5
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.exp
new file mode 100644
index 000000000000..cd4bc1ab64cc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.exp
@@ -0,0 +1 @@
+"hello world"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.nix
new file mode 100644
index 000000000000..125b0859ac42
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-interpol.nix
@@ -0,0 +1 @@
+"hello ${"world"}"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.exp
new file mode 100644
index 000000000000..00750edc07d6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.exp
@@ -0,0 +1 @@
+3
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.nix
new file mode 100644
index 000000000000..b440a220ff6d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-let.nix
@@ -0,0 +1,4 @@
+let
+  a = 1;
+  b = 2;
+in a + b
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.exp
new file mode 100644
index 000000000000..6db47b033efb
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.exp
@@ -0,0 +1 @@
+{ a = { b = 42; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.nix
new file mode 100644
index 000000000000..a97394d16523
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-nested-attrs.nix
@@ -0,0 +1 @@
+{ a.b = 42; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.nix
new file mode 100644
index 000000000000..c86ff80383c0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-recursive-attrs.nix
@@ -0,0 +1,4 @@
+(rec {
+  a = 21;
+  b = a * 2;
+}).b
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.nix
new file mode 100644
index 000000000000..64962b50ff63
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-simple-with.nix
@@ -0,0 +1,5 @@
+let
+  set = {
+    a = 1;
+  };
+in with set; a
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.exp
new file mode 100644
index 000000000000..9d783762143e
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.exp
@@ -0,0 +1 @@
+[ { index = 7; key = 0; } { index = 0; key = 1; } { index = 13; key = 1; } { index = 1; key = 2; } { index = 3; key = 2; } { index = 4; key = 2; } { index = 5; key = 2; } { index = 12; key = 2; } { index = 14; key = 2; } { index = 2; key = 3; } { index = 11; key = 3; } { index = 15; key = 3; } { index = 10; key = 4; } { index = 6; key = 5; } { index = 8; key = 5; } { index = 9; key = 5; } { index = 16; key = 22; } ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.nix
new file mode 100644
index 000000000000..9969e0a29433
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-stable-sort.nix
@@ -0,0 +1,7 @@
+let
+  keys = [ 1 2 3 2 2 2 5 0 5 5 4 3 2 1 2 3 22 ];
+in
+
+builtins.sort
+  (a: b: a.key < b.key)
+  (builtins.genList (index: { inherit index; key = builtins.elemAt keys index; }) (builtins.length keys))
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.nix
new file mode 100644
index 000000000000..f8eba2ac2fc0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-functor.nix
@@ -0,0 +1,7 @@
+let
+  __functor = f;
+  f = self: x: self.out * x;
+in {
+  inherit __functor;
+  out = 21;
+} 2
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.exp
new file mode 100644
index 000000000000..ffcd4415b08f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.exp
@@ -0,0 +1 @@
+{ }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.nix
new file mode 100644
index 000000000000..65e9e66d74dc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-if.nix
@@ -0,0 +1,6 @@
+let
+  a = {};
+in let
+  c = if builtins.isFunction a then a b else a;
+  b = {};
+in c
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.exp
new file mode 100644
index 000000000000..fc2f21e9305c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.exp
@@ -0,0 +1 @@
+"strict literal"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.nix
new file mode 100644
index 000000000000..bd3555bb2412
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-string-interpolation.nix
@@ -0,0 +1,7 @@
+let
+  final = { text = "strict literal"; inherit x y; };
+  x = "lazy ${throw "interpolation"}";
+  y = "${throw "also lazy!"}";
+in
+
+final.text
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.nix
new file mode 100644
index 000000000000..6f32660c4c33
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-thunked-with.nix
@@ -0,0 +1,7 @@
+# Creates a `with` across multiple thunk boundaries.
+
+let
+  set = {
+    a = with { b = 42; }; b;
+  };
+in set.a
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.exp
new file mode 100644
index 000000000000..edca9baca9c0
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.exp
@@ -0,0 +1 @@
+{ a = 1; b = 2; c = 3; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.nix
new file mode 100644
index 000000000000..4c6884bec3df
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-toplevel-finaliser.nix
@@ -0,0 +1,10 @@
+# A simple expression with upvalue resolution beyond the target stack
+# index of the root expression.
+
+let
+  a = 1;
+  b = 2;
+  c = 3;
+in {
+  inherit a b c;
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.exp
new file mode 100644
index 000000000000..8b6ed7dbac6b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.exp
@@ -0,0 +1 @@
+{ v = false; w = { success = false; value = false; }; x = { success = true; value = "x"; }; y = { success = false; value = false; }; z = { success = false; value = false; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.nix
new file mode 100644
index 000000000000..e2357c798753
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-tryeval.nix
@@ -0,0 +1,7 @@
+{
+  v = (builtins.tryEval (toString <oink>)).value;
+  w = builtins.tryEval <nope>;
+  x = builtins.tryEval "x";
+  y = builtins.tryEval (assert false; "y");
+  z = builtins.tryEval (throw "bla");
+}
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.exp
new file mode 100644
index 000000000000..5462431496bc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.exp
@@ -0,0 +1 @@
+[ true false null 1 2 3 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.nix
new file mode 100644
index 000000000000..30e9667da821
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-unpoison-scope.nix
@@ -0,0 +1,7 @@
+let
+  poisoned = let
+    true = 1;
+    false = 2;
+    null = 3;
+  in [ true false null ];
+in [ true false null ] ++ poisoned
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.exp
new file mode 100644
index 000000000000..d00491fd7e5b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.exp
@@ -0,0 +1 @@
+1
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.nix
new file mode 100644
index 000000000000..d335e5363000
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-useless-inherit-with.nix
@@ -0,0 +1,15 @@
+# Normally using an `inherit` without a source attribute set within a
+# `let` is a no-op, *unless* there is a with in-scope that might
+# provide the value.
+
+# Provide a dynamic `x` identifier in the scope.
+with ({ x = 1;});
+
+# inherit this `x` as a static identifier
+let inherit x;
+
+# Provide another dynamic `x` identifier
+in with ({ x = 3; });
+
+# Inherited static identifier should have precedence
+x
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.exp
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.exp
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.nix
new file mode 100644
index 000000000000..f60d27d38157
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-compare.nix
@@ -0,0 +1,6 @@
+# For an explanation of this behavior see //tvix/docs/value-pointer-equality.md
+let
+  f = owo: "thia";
+in
+
+[ f 42 ] > [ f 21 ]
\ No newline at end of file
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.exp
new file mode 100644
index 000000000000..aec30f297af9
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.exp
@@ -0,0 +1 @@
+[ true true true true false false false true true true true true true true true true false false ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.nix
new file mode 100644
index 000000000000..206881d7d9c2
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-value-pointer-equality.nix
@@ -0,0 +1,46 @@
+# For an explanation of this behavior see //tvix/docs/value-pointer-equality.md
+let
+  # Some incomparable values
+  f = MC: "Boing";
+  t = [ (throw "is a little blue man") ];
+  a = { "with" = abort "headphones and a big smile."; };
+
+  # Aliases
+  f' = f;
+  t' = t;
+  a' = a;
+
+  peq1 = a: b: [ a ] == [ b ];
+  peq2 = a: b: { x = a; } == { x = b; };
+in
+
+[
+  # pointer equality of functions
+  (peq1 f f)
+  (peq2 f f)
+  (peq1 f f')
+  (peq2 f f')
+
+  # encapsulation is necessary for pointer equality
+  (f == f)
+  (f == f')
+  # works with !=
+  ([ f ] != [ f' ])
+
+  # thunks that fail to evaluated wrapped in sets/lists
+  (peq1 t t)
+  (peq2 t t)
+  (peq1 a a)
+  (peq2 a a)
+  (peq1 t t')
+  (peq2 t t')
+  (peq1 a' a)
+  (peq2 a' a)
+
+  # function equality with builtins.elem
+  (builtins.elem f [ 21 f 42 ])
+
+  # pointer inequality
+  (peq1 f (x: x))
+  (peq2 (x: x) f)
+]
\ No newline at end of file
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.exp
new file mode 100644
index 000000000000..fa8f08cb6ff8
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.exp
@@ -0,0 +1 @@
+150
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.nix
new file mode 100644
index 000000000000..7e2f7c073bfc
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-with-closure.nix
@@ -0,0 +1,5 @@
+# Upvalues from `with` require special runtime handling. Do they work?
+let
+  f = with { a = 15; }; n: n * a;
+in
+f 10
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.exp
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.exp
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.nix
new file mode 100644
index 000000000000..c44455a5bf83
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-dynamic-key.nix
@@ -0,0 +1,12 @@
+# Tests correct tracking of stack indices within construction of an
+# attribute set. Dynamic keys can be any expression, so something that
+# is extremely sensitive to stack offsets (like `with`) can be tricky.
+
+let
+  set1 = { key = "b"; };
+  set2 = {
+    a = 20;
+    ${with set1; key} = 20;
+    ${with { key = "c"; }; key} = 2;
+  };
+in set2.a + set2.b + set2.c
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.exp
new file mode 100644
index 000000000000..5776134d0e41
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.exp
@@ -0,0 +1 @@
+[ 1 2 3 ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.nix
new file mode 100644
index 000000000000..bb62fdf31cd7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-with-in-list.nix
@@ -0,0 +1,13 @@
+# This code causes a situation where a list element causes an
+# additional phantom value to temporarily be placed on the locals
+# stack, which must be correctly accounted for by the compiler.
+
+let
+  set = {
+    value = 2;
+  };
+in [
+  1
+  (with set; value)
+  3
+]
diff --git a/tvix/eval/src/tests/tvix_tests/identity-bool-false.nix b/tvix/eval/src/tests/tvix_tests/identity-bool-false.nix
new file mode 100644
index 000000000000..c508d5366f70
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-bool-false.nix
@@ -0,0 +1 @@
+false
diff --git a/tvix/eval/src/tests/tvix_tests/identity-bool-true.nix b/tvix/eval/src/tests/tvix_tests/identity-bool-true.nix
new file mode 100644
index 000000000000..27ba77ddaf61
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-bool-true.nix
@@ -0,0 +1 @@
+true
diff --git a/tvix/eval/src/tests/tvix_tests/identity-dollar-escape.nix b/tvix/eval/src/tests/tvix_tests/identity-dollar-escape.nix
new file mode 100644
index 000000000000..08951d7637a6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-dollar-escape.nix
@@ -0,0 +1 @@
+"\${foobar}"
diff --git a/tvix/eval/src/tests/tvix_tests/identity-empty-attrs.nix b/tvix/eval/src/tests/tvix_tests/identity-empty-attrs.nix
new file mode 100644
index 000000000000..ffcd4415b08f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-empty-attrs.nix
@@ -0,0 +1 @@
+{ }
diff --git a/tvix/eval/src/tests/tvix_tests/identity-empty-list.nix b/tvix/eval/src/tests/tvix_tests/identity-empty-list.nix
new file mode 100644
index 000000000000..1e3ec7217afb
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-empty-list.nix
@@ -0,0 +1 @@
+[ ]
diff --git a/tvix/eval/src/tests/tvix_tests/identity-flat-attrs.nix b/tvix/eval/src/tests/tvix_tests/identity-flat-attrs.nix
new file mode 100644
index 000000000000..e7c2ae18a661
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-flat-attrs.nix
@@ -0,0 +1 @@
+{ a = 15; b = "string"; c = null; }
diff --git a/tvix/eval/src/tests/tvix_tests/identity-float.nix b/tvix/eval/src/tests/tvix_tests/identity-float.nix
new file mode 100644
index 000000000000..bf77d549685a
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-float.nix
@@ -0,0 +1 @@
+4.2
diff --git a/tvix/eval/src/tests/tvix_tests/identity-heterogeneous-list.nix b/tvix/eval/src/tests/tvix_tests/identity-heterogeneous-list.nix
new file mode 100644
index 000000000000..87f7fb0d0668
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-heterogeneous-list.nix
@@ -0,0 +1 @@
+[ 1 2.1 "three" null ]
diff --git a/tvix/eval/src/tests/tvix_tests/identity-homogeneous-float-list.nix b/tvix/eval/src/tests/tvix_tests/identity-homogeneous-float-list.nix
new file mode 100644
index 000000000000..48e6655fb169
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-homogeneous-float-list.nix
@@ -0,0 +1 @@
+[ 4.2 6.9 13.37 ]
diff --git a/tvix/eval/src/tests/tvix_tests/identity-homogeneous-int-list.nix b/tvix/eval/src/tests/tvix_tests/identity-homogeneous-int-list.nix
new file mode 100644
index 000000000000..d23a5c38147b
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-homogeneous-int-list.nix
@@ -0,0 +1 @@
+[ 0 1 2 3 4 5 7 8 9 ]
diff --git a/tvix/eval/src/tests/tvix_tests/identity-homogeneous-string-list.nix b/tvix/eval/src/tests/tvix_tests/identity-homogeneous-string-list.nix
new file mode 100644
index 000000000000..d78a54e5b002
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-homogeneous-string-list.nix
@@ -0,0 +1 @@
+[ "string" "list" ]
diff --git a/tvix/eval/src/tests/tvix_tests/identity-int.nix b/tvix/eval/src/tests/tvix_tests/identity-int.nix
new file mode 100644
index 000000000000..d81cc0710eb6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-int.nix
@@ -0,0 +1 @@
+42
diff --git a/tvix/eval/src/tests/tvix_tests/identity-kv-attrs.nix b/tvix/eval/src/tests/tvix_tests/identity-kv-attrs.nix
new file mode 100644
index 000000000000..f1398b8d0592
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-kv-attrs.nix
@@ -0,0 +1 @@
+{ name = "foo"; value = 12; }
diff --git a/tvix/eval/src/tests/tvix_tests/identity-nested-attrs.nix b/tvix/eval/src/tests/tvix_tests/identity-nested-attrs.nix
new file mode 100644
index 000000000000..6a139452ef28
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-nested-attrs.nix
@@ -0,0 +1 @@
+{ a = { b = null; }; }
diff --git a/tvix/eval/src/tests/tvix_tests/identity-null.nix b/tvix/eval/src/tests/tvix_tests/identity-null.nix
new file mode 100644
index 000000000000..19765bd501b6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-null.nix
@@ -0,0 +1 @@
+null
diff --git a/tvix/eval/src/tests/tvix_tests/identity-signed-float.nix b/tvix/eval/src/tests/tvix_tests/identity-signed-float.nix
new file mode 100644
index 000000000000..50c9d06aa52c
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-signed-float.nix
@@ -0,0 +1 @@
+-4.2
diff --git a/tvix/eval/src/tests/tvix_tests/identity-signed-int.nix b/tvix/eval/src/tests/tvix_tests/identity-signed-int.nix
new file mode 100644
index 000000000000..6a0e60d48b17
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-signed-int.nix
@@ -0,0 +1 @@
+-42
diff --git a/tvix/eval/src/tests/tvix_tests/identity-string.nix b/tvix/eval/src/tests/tvix_tests/identity-string.nix
new file mode 100644
index 000000000000..d71ddbcf823f
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/identity-string.nix
@@ -0,0 +1 @@
+"test string"
diff --git a/tvix/eval/src/tests/tvix_tests/lib.nix b/tvix/eval/src/tests/tvix_tests/lib.nix
new file mode 100644
index 000000000000..028a538314b7
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/lib.nix
@@ -0,0 +1,61 @@
+with builtins;
+
+rec {
+
+  fold = op: nul: list:
+    if list == []
+    then nul
+    else op (head list) (fold op nul (tail list));
+
+  concat =
+    fold (x: y: x + y) "";
+
+  and = fold (x: y: x && y) true;
+
+  flatten = x:
+    if isList x
+    then fold (x: y: (flatten x) ++ y) [] x
+    else [x];
+
+  sum = foldl' (x: y: add x y) 0;
+
+  hasSuffix = ext: fileName:
+    let lenFileName = stringLength fileName;
+        lenExt = stringLength ext;
+    in !(lessThan lenFileName lenExt) &&
+       substring (sub lenFileName lenExt) lenFileName fileName == ext;
+
+  # Split a list at the given position.
+  splitAt = pos: list:
+    if pos == 0 then {first = []; second = list;} else
+    if list == [] then {first = []; second = [];} else
+    let res = splitAt (sub pos 1) (tail list);
+    in {first = [(head list)] ++ res.first; second = res.second;};
+
+  # Stable merge sort.
+  sortBy = comp: list:
+    if lessThan 1 (length list)
+    then
+      let
+        split = splitAt (div (length list) 2) list;
+        first = sortBy comp split.first;
+        second = sortBy comp split.second;
+      in mergeLists comp first second
+    else list;
+
+  mergeLists = comp: list1: list2:
+    if list1 == [] then list2 else
+    if list2 == [] then list1 else
+    if comp (head list2) (head list1) then [(head list2)] ++ mergeLists comp list1 (tail list2) else
+    [(head list1)] ++ mergeLists comp (tail list1) list2;
+
+  id = x: x;
+
+  const = x: y: x;
+
+  range = first: last:
+    if first > last
+      then []
+      else genList (n: first + n) (last - first + 1);
+
+}
diff --git a/tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.exp b/tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.exp
new file mode 100644
index 000000000000..967fc858bc3d
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.exp
@@ -0,0 +1 @@
+[ "abort" "add" "addErrorContext" "all" "any" "appendContext" "attrNames" "attrValues" "baseNameOf" "bitAnd" "bitOr" "bitXor" "builtins" "catAttrs" "compareVersions" "concatLists" "concatMap" "concatStringsSep" "currentSystem" "currentTime" "deepSeq" "derivation" "derivationStrict" "dirOf" "div" "elem" "elemAt" "false" "fetchGit" "fetchMercurial" "fetchTarball" "fetchurl" "filter" "filterSource" "findFile" "foldl'" "fromJSON" "fromTOML" "functionArgs" "genList" "genericClosure" "getAttr" "getContext" "getEnv" "hasAttr" "hasContext" "hashFile" "hashString" "head" "import" "intersectAttrs" "isAttrs" "isBool" "isFloat" "isFunction" "isInt" "isList" "isNull" "isPath" "isString" "langVersion" "length" "lessThan" "listToAttrs" "map" "mapAttrs" "match" "mul" "nixPath" "nixVersion" "null" "parseDrvName" "partition" "path" "pathExists" "placeholder" "readDir" "readFile" "removeAttrs" "replaceStrings" "scopedImport" "seq" "sort" "split" "splitVersion" "storeDir" "storePath" "stringLength" "sub" "substring" "tail" "throw" "toFile" "toJSON" "toPath" "toString" "toXML" "trace" "true" "tryEval" "typeOf" "unsafeDiscardOutputDependency" "unsafeDiscardStringContext" "unsafeGetAttrPos" ]
diff --git a/tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.nix b/tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.nix
new file mode 100644
index 000000000000..804355f34d10
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/notyetpassing/eval-okay-minimal-2.3-builtins.nix
@@ -0,0 +1,35 @@
+# This tests verifies that the Nix implementation evaluating this has at least
+# all the builtins given in `minimalBuiltins`. We don't test a precise list of
+# builtins since we accept that there will always be difference between the
+# builtins sets of Tvix, C++ Nix 2.3 and newer C++ Nix versions, as new builtins
+# are added.
+#
+# Tvix also may choose never to implement some builtins if they are only useful
+# for flakes or perform well enough via the shims nixpkgs usually provides.
+
+let
+  # C++ Nix 2.3 builtins except valueSize which is removed in later versions
+  minimalBuiltins = [
+    "abort" "add" "addErrorContext" "all" "any" "appendContext" "attrNames"
+    "attrValues" "baseNameOf" "bitAnd" "bitOr" "bitXor" "builtins" "catAttrs"
+    "compareVersions" "concatLists" "concatMap" "concatStringsSep"
+    "currentSystem" "currentTime" "deepSeq" "derivation" "derivationStrict"
+    "dirOf" "div" "elem" "elemAt" "false" "fetchGit" "fetchMercurial"
+    "fetchTarball" "fetchurl" "filter" "filterSource" "findFile" "foldl'"
+    "fromJSON" "fromTOML" "functionArgs" "genList" "genericClosure" "getAttr"
+    "getContext" "getEnv" "hasAttr" "hasContext" "hashFile" "hashString" "head"
+    "import" "intersectAttrs" "isAttrs" "isBool" "isFloat" "isFunction" "isInt"
+    "isList" "isNull" "isPath" "isString" "langVersion" "length" "lessThan"
+    "listToAttrs" "map" "mapAttrs" "match" "mul" "nixPath" "nixVersion" "null"
+    "parseDrvName" "partition" "path" "pathExists" "placeholder" "readDir"
+    "readFile" "removeAttrs" "replaceStrings" "scopedImport" "seq" "sort"
+    "split" "splitVersion" "storeDir" "storePath" "stringLength" "sub"
+    "substring" "tail" "throw" "toFile" "toJSON" "toPath" "toString" "toXML"
+    "trace" "true" "tryEval" "typeOf" "unsafeDiscardOutputDependency"
+    "unsafeDiscardStringContext" "unsafeGetAttrPos"
+  ];
+
+  intersectLists = as: bs: builtins.filter (a: builtins.elem a bs) as;
+in
+
+intersectLists minimalBuiltins (builtins.attrNames builtins)
diff --git a/tvix/eval/src/tests/tvix_tests/readDir/bar b/tvix/eval/src/tests/tvix_tests/readDir/bar
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/readDir/bar
diff --git a/tvix/eval/src/tests/tvix_tests/readDir/foo/.keep b/tvix/eval/src/tests/tvix_tests/readDir/foo/.keep
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/readDir/foo/.keep
diff --git a/tvix/eval/src/upvalues.rs b/tvix/eval/src/upvalues.rs
new file mode 100644
index 000000000000..687d6850ccfa
--- /dev/null
+++ b/tvix/eval/src/upvalues.rs
@@ -0,0 +1,86 @@
+//! This module encapsulates some logic for upvalue handling, which is
+//! relevant to both thunks (delayed computations for lazy-evaluation)
+//! as well as closures (lambdas that capture variables from the
+//! surrounding scope).
+//!
+//! The upvalues of a scope are whatever data are needed at runtime
+//! in order to resolve each free variable in the scope to a value.
+//! "Upvalue" is a term taken from Lua.
+
+use std::ops::Index;
+
+use crate::{opcode::UpvalueIdx, Value};
+
+/// Structure for carrying upvalues of an UpvalueCarrier.  The
+/// implementation of this struct encapsulates the logic for
+/// capturing and accessing upvalues.
+///
+/// Nix's `with` cannot be used to shadow an enclosing binding --
+/// like Rust's `use xyz::*` construct, but unlike Javascript's
+/// `with (xyz)`.  This means that Nix has two kinds of identifiers,
+/// which can be distinguished at compile time:
+///
+/// - Static identifiers, which are bound in some enclosing scope by
+///   `let`, `name:` or `{name}:`
+/// - Dynamic identifiers, which are not bound in any enclosing
+///   scope
+#[derive(Clone, Debug)]
+pub struct Upvalues {
+    /// The upvalues of static identifiers.  Each static identifier
+    /// is assigned an integer identifier at compile time, which is
+    /// an index into this Vec.
+    static_upvalues: Vec<Value>,
+
+    /// The upvalues of dynamic identifiers, if any exist.  This
+    /// consists of the value passed to each enclosing `with val;`,
+    /// from outermost to innermost.
+    with_stack: Option<Vec<Value>>,
+}
+
+impl Upvalues {
+    pub fn with_capacity(count: usize) -> Self {
+        Upvalues {
+            static_upvalues: Vec::with_capacity(count),
+            with_stack: None,
+        }
+    }
+
+    /// Push an upvalue at the end of the upvalue list.
+    pub fn push(&mut self, value: Value) {
+        self.static_upvalues.push(value);
+    }
+
+    /// Set the captured with stack.
+    pub fn set_with_stack(&mut self, with_stack: Vec<Value>) {
+        self.with_stack = Some(with_stack);
+    }
+
+    pub fn with_stack(&self) -> Option<&Vec<Value>> {
+        self.with_stack.as_ref()
+    }
+
+    pub fn with_stack_len(&self) -> usize {
+        match &self.with_stack {
+            None => 0,
+            Some(stack) => stack.len(),
+        }
+    }
+
+    /// Resolve deferred upvalues from the provided stack slice,
+    /// mutating them in the internal upvalue slots.
+    pub fn resolve_deferred_upvalues(&mut self, stack: &[Value]) {
+        for upvalue in self.static_upvalues.iter_mut() {
+            if let Value::DeferredUpvalue(update_from_idx) = upvalue {
+                *upvalue = stack[update_from_idx.0].clone();
+            }
+        }
+    }
+}
+
+impl Index<UpvalueIdx> for Upvalues {
+    type Output = Value;
+
+    fn index(&self, index: UpvalueIdx) -> &Self::Output {
+        &self.static_upvalues[index.0]
+    }
+}
diff --git a/tvix/eval/src/value/arbitrary.rs b/tvix/eval/src/value/arbitrary.rs
new file mode 100644
index 000000000000..bf53f4fcb28a
--- /dev/null
+++ b/tvix/eval/src/value/arbitrary.rs
@@ -0,0 +1,106 @@
+//! Support for configurable generation of arbitrary nix values
+
+use imbl::proptest::{ord_map, vector};
+use proptest::{prelude::*, strategy::BoxedStrategy};
+use std::ffi::OsString;
+
+use super::{attrs::AttrsRep, NixAttrs, NixList, NixString, Value};
+
+#[derive(Clone)]
+pub enum Parameters {
+    Strategy(BoxedStrategy<Value>),
+    Parameters {
+        generate_internal_values: bool,
+        generate_functions: bool,
+        generate_nested: bool,
+    },
+}
+
+impl Default for Parameters {
+    fn default() -> Self {
+        Self::Parameters {
+            generate_internal_values: false,
+            generate_functions: false,
+            generate_nested: true,
+        }
+    }
+}
+
+impl Arbitrary for NixAttrs {
+    type Parameters = Parameters;
+    type Strategy = BoxedStrategy<Self>;
+
+    fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
+        prop_oneof![
+            // Empty attrs representation
+            Just(Self(AttrsRep::Empty)),
+            // KV representation (name/value pairs)
+            (
+                any_with::<Value>(args.clone()),
+                any_with::<Value>(args.clone())
+            )
+                .prop_map(|(name, value)| Self(AttrsRep::KV { name, value })),
+            // Map representation
+            ord_map(NixString::arbitrary(), Value::arbitrary_with(args), 0..100)
+                .prop_map(|map| Self(AttrsRep::Im(map)))
+        ]
+        .boxed()
+    }
+}
+
+impl Arbitrary for NixList {
+    type Parameters = <Value as Arbitrary>::Parameters;
+    type Strategy = BoxedStrategy<Self>;
+
+    fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
+        vector(<Value as Arbitrary>::arbitrary_with(args), 0..100)
+            .prop_map(NixList::from)
+            .boxed()
+    }
+}
+
+impl Arbitrary for Value {
+    type Parameters = Parameters;
+    type Strategy = BoxedStrategy<Self>;
+
+    fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
+        match args {
+            Parameters::Strategy(s) => s,
+            Parameters::Parameters {
+                generate_internal_values,
+                generate_functions,
+                generate_nested,
+            } => {
+                if generate_internal_values || generate_functions {
+                    todo!("Generating internal values and functions not implemented yet")
+                } else if generate_nested {
+                    non_internal_value().boxed()
+                } else {
+                    leaf_value().boxed()
+                }
+            }
+        }
+    }
+}
+
+fn leaf_value() -> impl Strategy<Value = Value> {
+    use Value::*;
+
+    prop_oneof![
+        Just(Null),
+        any::<bool>().prop_map(Bool),
+        any::<i64>().prop_map(Integer),
+        any::<f64>().prop_map(Float),
+        any::<NixString>().prop_map(String),
+        any::<OsString>().prop_map(|s| Path(Box::new(s.into()))),
+    ]
+}
+
+fn non_internal_value() -> impl Strategy<Value = Value> {
+    leaf_value().prop_recursive(3, 5, 5, |inner| {
+        prop_oneof![
+            NixAttrs::arbitrary_with(Parameters::Strategy(inner.clone())).prop_map(Value::attrs),
+            any_with::<NixList>(Parameters::Strategy(inner)).prop_map(Value::List)
+        ]
+    })
+}
diff --git a/tvix/eval/src/value/attrs.rs b/tvix/eval/src/value/attrs.rs
new file mode 100644
index 000000000000..bacfd22217bd
--- /dev/null
+++ b/tvix/eval/src/value/attrs.rs
@@ -0,0 +1,609 @@
+//! This module implements Nix attribute sets. They have flexible
+//! backing implementations, as they are used in very versatile
+//! use-cases that are all exposed the same way in the language
+//! surface.
+//!
+//! Due to this, construction and management of attribute sets has
+//! some peculiarities that are encapsulated within this module.
+use std::iter::FromIterator;
+
+use imbl::{ordmap, OrdMap};
+use lazy_static::lazy_static;
+use serde::de::{Deserializer, Error, Visitor};
+use serde::Deserialize;
+
+use super::string::NixString;
+use super::thunk::ThunkSet;
+use super::CoercionKind;
+use super::TotalDisplay;
+use super::Value;
+use crate::errors::ErrorKind;
+use crate::generators::{self, GenCo};
+
+lazy_static! {
+    static ref NAME_S: NixString = "name".into();
+    static ref NAME_REF: &'static NixString = &NAME_S;
+    static ref VALUE_S: NixString = "value".into();
+    static ref VALUE_REF: &'static NixString = &VALUE_S;
+}
+
+#[cfg(test)]
+mod tests;
+
+#[derive(Clone, Debug, Deserialize)]
+pub(super) enum AttrsRep {
+    Empty,
+
+    Im(OrdMap<NixString, Value>),
+
+    /// Warning: this represents a **two**-attribute attrset, with
+    /// attribute names "name" and "value", like `{name="foo";
+    /// value="bar";}`, *not* `{foo="bar";}`!
+    KV {
+        name: Value,
+        value: Value,
+    },
+}
+
+impl Default for AttrsRep {
+    fn default() -> Self {
+        AttrsRep::Empty
+    }
+}
+
+impl AttrsRep {
+    /// Retrieve reference to a mutable map inside of an attrs,
+    /// optionally changing the representation if required.
+    fn map_mut(&mut self) -> &mut OrdMap<NixString, Value> {
+        match self {
+            AttrsRep::Im(m) => m,
+
+            AttrsRep::Empty => {
+                *self = AttrsRep::Im(OrdMap::new());
+                self.map_mut()
+            }
+
+            AttrsRep::KV { name, value } => {
+                *self = AttrsRep::Im(ordmap! {
+                    NAME_S.clone() => name.clone(),
+                    VALUE_S.clone() => value.clone()
+                });
+
+                self.map_mut()
+            }
+        }
+    }
+
+    fn select(&self, key: &str) -> Option<&Value> {
+        match self {
+            AttrsRep::Empty => None,
+
+            AttrsRep::KV { name, value } => match key {
+                "name" => Some(name),
+                "value" => Some(value),
+                _ => None,
+            },
+
+            AttrsRep::Im(map) => map.get(&key.into()),
+        }
+    }
+
+    fn contains(&self, key: &str) -> bool {
+        match self {
+            AttrsRep::Empty => false,
+            AttrsRep::KV { .. } => key == "name" || key == "value",
+            AttrsRep::Im(map) => map.contains_key(&key.into()),
+        }
+    }
+}
+
+#[repr(transparent)]
+#[derive(Clone, Debug, Default)]
+pub struct NixAttrs(pub(super) AttrsRep);
+
+impl From<OrdMap<NixString, Value>> for NixAttrs {
+    fn from(map: OrdMap<NixString, Value>) -> Self {
+        NixAttrs(AttrsRep::Im(map))
+    }
+}
+
+impl<K, V> FromIterator<(K, V)> for NixAttrs
+where
+    NixString: From<K>,
+    Value: From<V>,
+{
+    fn from_iter<T>(iter: T) -> NixAttrs
+    where
+        T: IntoIterator<Item = (K, V)>,
+    {
+        NixAttrs(AttrsRep::Im(iter.into_iter().collect()))
+    }
+}
+
+impl TotalDisplay for NixAttrs {
+    fn total_fmt(&self, f: &mut std::fmt::Formatter<'_>, set: &mut ThunkSet) -> std::fmt::Result {
+        f.write_str("{ ")?;
+
+        match &self.0 {
+            AttrsRep::KV { name, value } => {
+                f.write_str("name = ")?;
+                name.total_fmt(f, set)?;
+                f.write_str("; ")?;
+
+                f.write_str("value = ")?;
+                value.total_fmt(f, set)?;
+                f.write_str("; ")?;
+            }
+
+            AttrsRep::Im(map) => {
+                for (name, value) in map {
+                    write!(f, "{} = ", name.ident_str())?;
+                    value.total_fmt(f, set)?;
+                    f.write_str("; ")?;
+                }
+            }
+
+            AttrsRep::Empty => { /* no values to print! */ }
+        }
+
+        f.write_str("}")
+    }
+}
+
+impl<'de> Deserialize<'de> for NixAttrs {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct MapVisitor;
+
+        impl<'de> Visitor<'de> for MapVisitor {
+            type Value = NixAttrs;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a valid Nix attribute set")
+            }
+
+            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
+            where
+                A: serde::de::MapAccess<'de>,
+            {
+                let mut stack_array = Vec::with_capacity(map.size_hint().unwrap_or(0) * 2);
+
+                while let Some((key, value)) = map.next_entry()? {
+                    stack_array.push(key);
+                    stack_array.push(value);
+                }
+
+                NixAttrs::construct(stack_array.len() / 2, stack_array).map_err(A::Error::custom)
+            }
+        }
+
+        deserializer.deserialize_map(MapVisitor)
+    }
+}
+
+impl NixAttrs {
+    pub fn empty() -> Self {
+        Self(AttrsRep::Empty)
+    }
+
+    /// Compare two attribute sets by pointer equality. Only makes
+    /// sense for some attribute set reprsentations, i.e. returning
+    /// `false` does not mean that the two attribute sets do not have
+    /// equal *content*.
+    pub fn ptr_eq(&self, other: &Self) -> bool {
+        match (&self.0, &other.0) {
+            (AttrsRep::Im(lhs), AttrsRep::Im(rhs)) => lhs.ptr_eq(rhs),
+            _ => false,
+        }
+    }
+
+    /// Return an attribute set containing the merge of the two
+    /// provided sets. Keys from the `other` set have precedence.
+    pub fn update(self, other: Self) -> Self {
+        // Short-circuit on some optimal cases:
+        match (&self.0, &other.0) {
+            (AttrsRep::Empty, AttrsRep::Empty) => return self,
+            (AttrsRep::Empty, _) => return other,
+            (_, AttrsRep::Empty) => return self,
+            (AttrsRep::KV { .. }, AttrsRep::KV { .. }) => return other,
+
+            // Explicitly handle all branches instead of falling
+            // through, to ensure that we get at least some compiler
+            // errors if variants are modified.
+            (AttrsRep::Im(_), AttrsRep::Im(_))
+            | (AttrsRep::Im(_), AttrsRep::KV { .. })
+            | (AttrsRep::KV { .. }, AttrsRep::Im(_)) => {}
+        };
+
+        // Slightly more advanced, but still optimised updates
+        match (self.0, other.0) {
+            (AttrsRep::Im(mut m), AttrsRep::KV { name, value }) => {
+                m.insert(NAME_S.clone(), name);
+                m.insert(VALUE_S.clone(), value);
+                NixAttrs(AttrsRep::Im(m))
+            }
+
+            (AttrsRep::KV { name, value }, AttrsRep::Im(mut m)) => {
+                match m.entry(NAME_S.clone()) {
+                    imbl::ordmap::Entry::Vacant(e) => {
+                        e.insert(name);
+                    }
+
+                    imbl::ordmap::Entry::Occupied(_) => { /* name from `m` has precedence */ }
+                };
+
+                match m.entry(VALUE_S.clone()) {
+                    imbl::ordmap::Entry::Vacant(e) => {
+                        e.insert(value);
+                    }
+
+                    imbl::ordmap::Entry::Occupied(_) => { /* value from `m` has precedence */ }
+                };
+
+                NixAttrs(AttrsRep::Im(m))
+            }
+
+            // Plain merge of maps.
+            (AttrsRep::Im(m1), AttrsRep::Im(m2)) => NixAttrs(AttrsRep::Im(m2.union(m1))),
+
+            // Cases handled above by the borrowing match:
+            _ => unreachable!(),
+        }
+    }
+
+    /// Return the number of key-value entries in an attrset.
+    pub fn len(&self) -> usize {
+        match &self.0 {
+            AttrsRep::Im(map) => map.len(),
+            AttrsRep::Empty => 0,
+            AttrsRep::KV { .. } => 2,
+        }
+    }
+
+    pub fn is_empty(&self) -> bool {
+        match &self.0 {
+            AttrsRep::Im(map) => map.is_empty(),
+            AttrsRep::Empty => true,
+            AttrsRep::KV { .. } => false,
+        }
+    }
+
+    /// Select a value from an attribute set by key.
+    pub fn select(&self, key: &str) -> Option<&Value> {
+        self.0.select(key)
+    }
+
+    /// Select a required value from an attribute set by key, return
+    /// an `AttributeNotFound` error if it is missing.
+    pub fn select_required(&self, key: &str) -> Result<&Value, ErrorKind> {
+        self.select(key)
+            .ok_or_else(|| ErrorKind::AttributeNotFound { name: key.into() })
+    }
+
+    pub fn contains(&self, key: &str) -> bool {
+        self.0.contains(key)
+    }
+
+    /// Construct an iterator over all the key-value pairs in the attribute set.
+    #[allow(clippy::needless_lifetimes)]
+    pub fn iter<'a>(&'a self) -> Iter<KeyValue<'a>> {
+        Iter(match &self.0 {
+            AttrsRep::Im(map) => KeyValue::Im(map.iter()),
+            AttrsRep::Empty => KeyValue::Empty,
+
+            AttrsRep::KV {
+                ref name,
+                ref value,
+            } => KeyValue::KV {
+                name,
+                value,
+                at: IterKV::default(),
+            },
+        })
+    }
+
+    pub fn into_iter(self) -> IntoIter {
+        match self.0 {
+            AttrsRep::Empty => IntoIter(IntoIterRepr::Empty),
+            AttrsRep::KV { name, value } => IntoIter(IntoIterRepr::Finite(
+                vec![(NAME_REF.clone(), name), (VALUE_REF.clone(), value)].into_iter(),
+            )),
+            AttrsRep::Im(map) => IntoIter(IntoIterRepr::Im(map.into_iter())),
+        }
+    }
+
+    /// Same as into_iter(), but marks call sites which rely on the
+    /// iteration being lexicographic.
+    pub fn into_iter_sorted(self) -> IntoIter {
+        self.into_iter()
+    }
+
+    /// Construct an iterator over all the keys of the attribute set
+    pub fn keys(&self) -> Keys {
+        Keys(match &self.0 {
+            AttrsRep::Empty => KeysInner::Empty,
+            AttrsRep::Im(m) => KeysInner::Im(m.keys()),
+            AttrsRep::KV { .. } => KeysInner::KV(IterKV::default()),
+        })
+    }
+
+    /// Implement construction logic of an attribute set, to encapsulate
+    /// logic about attribute set optimisations inside of this module.
+    pub fn construct(count: usize, mut stack_slice: Vec<Value>) -> Result<Self, ErrorKind> {
+        debug_assert!(
+            stack_slice.len() == count * 2,
+            "construct_attrs called with count == {}, but slice.len() == {}",
+            count,
+            stack_slice.len(),
+        );
+
+        // Optimisation: Empty attribute set
+        if count == 0 {
+            return Ok(NixAttrs(AttrsRep::Empty));
+        }
+
+        // Optimisation: KV pattern
+        if count == 2 {
+            if let Some(kv) = attempt_optimise_kv(&mut stack_slice) {
+                return Ok(kv);
+            }
+        }
+
+        let mut attrs = NixAttrs(AttrsRep::Im(OrdMap::new()));
+
+        for _ in 0..count {
+            let value = stack_slice.pop().unwrap();
+            let key = stack_slice.pop().unwrap();
+
+            match key {
+                Value::String(ks) => set_attr(&mut attrs, ks, value)?,
+
+                Value::Null => {
+                    // This is in fact valid, but leads to the value
+                    // being ignored and nothing being set, i.e. `{
+                    // ${null} = 1; } => { }`.
+                    continue;
+                }
+
+                other => return Err(ErrorKind::InvalidAttributeName(other)),
+            }
+        }
+
+        Ok(attrs)
+    }
+
+    /// Construct an optimized "KV"-style attribute set given the value for the
+    /// `"name"` key, and the value for the `"value"` key
+    pub(crate) fn from_kv(name: Value, value: Value) -> Self {
+        NixAttrs(AttrsRep::KV { name, value })
+    }
+
+    /// Attempt to coerce an attribute set with a `__toString`
+    /// attribute to a string.
+    pub(crate) async fn try_to_string(&self, co: &GenCo, kind: CoercionKind) -> Option<NixString> {
+        if let Some(to_string) = self.select("__toString") {
+            let callable = generators::request_force(&co, to_string.clone()).await;
+
+            // Leave the attribute set on the stack as an argument
+            // to the function call.
+            generators::request_stack_push(&co, Value::Attrs(Box::new(self.clone()))).await;
+
+            // Call the callable ...
+            let result = generators::request_call(&co, callable).await;
+
+            // Recurse on the result, as attribute set coercion
+            // actually works recursively, e.g. you can even return
+            // /another/ set with a __toString attr.
+            let s = generators::request_string_coerce(&co, result, kind).await;
+
+            return Some(s);
+        }
+
+        None
+    }
+}
+
+/// In Nix, name/value attribute pairs are frequently constructed from
+/// literals. This particular case should avoid allocation of a map,
+/// additional heap values etc. and use the optimised `KV` variant
+/// instead.
+///
+/// ```norust
+/// `slice` is the top of the stack from which the attrset is being
+/// constructed, e.g.
+///
+///   slice: [ "value" 5 "name" "foo" ]
+///   index:   0       1 2      3
+///   stack:   3       2 1      0
+/// ```
+fn attempt_optimise_kv(slice: &mut [Value]) -> Option<NixAttrs> {
+    let (name_idx, value_idx) = {
+        match (&slice[2], &slice[0]) {
+            (Value::String(s1), Value::String(s2)) if (*s1 == *NAME_S && *s2 == *VALUE_S) => (3, 1),
+
+            (Value::String(s1), Value::String(s2)) if (*s1 == *VALUE_S && *s2 == *NAME_S) => (1, 3),
+
+            // Technically this branch lets type errors pass,
+            // but they will be caught during normal attribute
+            // set construction instead.
+            _ => return None,
+        }
+    };
+
+    Some(NixAttrs::from_kv(
+        slice[name_idx].clone(),
+        slice[value_idx].clone(),
+    ))
+}
+
+/// Set an attribute on an in-construction attribute set, while
+/// checking against duplicate keys.
+fn set_attr(attrs: &mut NixAttrs, key: NixString, value: Value) -> Result<(), ErrorKind> {
+    match attrs.0.map_mut().entry(key) {
+        imbl::ordmap::Entry::Occupied(entry) => Err(ErrorKind::DuplicateAttrsKey {
+            key: entry.key().as_str().to_string(),
+        }),
+
+        imbl::ordmap::Entry::Vacant(entry) => {
+            entry.insert(value);
+            Ok(())
+        }
+    }
+}
+
+/// Internal helper type to track the iteration status of an iterator
+/// over the name/value representation.
+#[derive(Debug, Default)]
+pub enum IterKV {
+    #[default]
+    Name,
+    Value,
+    Done,
+}
+
+impl IterKV {
+    fn next(&mut self) {
+        match *self {
+            Self::Name => *self = Self::Value,
+            Self::Value => *self = Self::Done,
+            Self::Done => {}
+        }
+    }
+}
+
+/// Iterator representation over the keys *and* values of an attribute
+/// set.
+pub enum KeyValue<'a> {
+    Empty,
+
+    KV {
+        name: &'a Value,
+        value: &'a Value,
+        at: IterKV,
+    },
+
+    Im(imbl::ordmap::Iter<'a, NixString, Value>),
+}
+
+/// Iterator over a Nix attribute set.
+// This wrapper type exists to make the inner "raw" iterator
+// inaccessible.
+#[repr(transparent)]
+pub struct Iter<T>(T);
+
+impl<'a> Iterator for Iter<KeyValue<'a>> {
+    type Item = (&'a NixString, &'a Value);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match &mut self.0 {
+            KeyValue::Im(inner) => inner.next(),
+            KeyValue::Empty => None,
+
+            KeyValue::KV { name, value, at } => match at {
+                IterKV::Name => {
+                    at.next();
+                    Some((&NAME_REF, name))
+                }
+
+                IterKV::Value => {
+                    at.next();
+                    Some((&VALUE_REF, value))
+                }
+
+                IterKV::Done => None,
+            },
+        }
+    }
+}
+
+impl<'a> ExactSizeIterator for Iter<KeyValue<'a>> {
+    fn len(&self) -> usize {
+        match &self.0 {
+            KeyValue::Empty => 0,
+            KeyValue::KV { .. } => 2,
+            KeyValue::Im(inner) => inner.len(),
+        }
+    }
+}
+
+enum KeysInner<'a> {
+    Empty,
+    KV(IterKV),
+    Im(imbl::ordmap::Keys<'a, NixString, Value>),
+}
+
+pub struct Keys<'a>(KeysInner<'a>);
+
+impl<'a> Iterator for Keys<'a> {
+    type Item = &'a NixString;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match &mut self.0 {
+            KeysInner::Empty => None,
+            KeysInner::KV(at @ IterKV::Name) => {
+                at.next();
+                Some(&NAME_REF)
+            }
+            KeysInner::KV(at @ IterKV::Value) => {
+                at.next();
+                Some(&VALUE_REF)
+            }
+            KeysInner::KV(IterKV::Done) => None,
+            KeysInner::Im(m) => m.next(),
+        }
+    }
+}
+
+impl<'a> IntoIterator for &'a NixAttrs {
+    type Item = (&'a NixString, &'a Value);
+
+    type IntoIter = Iter<KeyValue<'a>>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter()
+    }
+}
+
+impl<'a> ExactSizeIterator for Keys<'a> {
+    fn len(&self) -> usize {
+        match &self.0 {
+            KeysInner::Empty => 0,
+            KeysInner::KV(_) => 2,
+            KeysInner::Im(m) => m.len(),
+        }
+    }
+}
+
+/// Internal representation of an owning attrset iterator
+pub enum IntoIterRepr {
+    Empty,
+    Finite(std::vec::IntoIter<(NixString, Value)>),
+    Im(imbl::ordmap::ConsumingIter<(NixString, Value)>),
+}
+
+#[repr(transparent)]
+pub struct IntoIter(IntoIterRepr);
+
+impl Iterator for IntoIter {
+    type Item = (NixString, Value);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match &mut self.0 {
+            IntoIterRepr::Empty => None,
+            IntoIterRepr::Finite(inner) => inner.next(),
+            IntoIterRepr::Im(inner) => inner.next(),
+        }
+    }
+}
+
+impl ExactSizeIterator for IntoIter {
+    fn len(&self) -> usize {
+        match &self.0 {
+            IntoIterRepr::Empty => 0,
+            IntoIterRepr::Finite(inner) => inner.len(),
+            IntoIterRepr::Im(inner) => inner.len(),
+        }
+    }
+}
diff --git a/tvix/eval/src/value/attrs/tests.rs b/tvix/eval/src/value/attrs/tests.rs
new file mode 100644
index 000000000000..473592c519df
--- /dev/null
+++ b/tvix/eval/src/value/attrs/tests.rs
@@ -0,0 +1,108 @@
+use super::*;
+
+#[test]
+fn test_empty_attrs() {
+    let attrs = NixAttrs::construct(0, vec![]).expect("empty attr construction should succeed");
+
+    assert!(
+        matches!(attrs, NixAttrs(AttrsRep::Empty)),
+        "empty attribute set should use optimised representation"
+    );
+}
+
+#[test]
+fn test_simple_attrs() {
+    let attrs = NixAttrs::construct(
+        1,
+        vec![Value::String("key".into()), Value::String("value".into())],
+    )
+    .expect("simple attr construction should succeed");
+
+    assert!(
+        matches!(attrs, NixAttrs(AttrsRep::Im(_))),
+        "simple attribute set should use map representation",
+    )
+}
+
+#[test]
+fn test_kv_attrs() {
+    let name_val = Value::String("name".into());
+    let value_val = Value::String("value".into());
+    let meaning_val = Value::String("meaning".into());
+    let forty_two_val = Value::Integer(42);
+
+    let kv_attrs = NixAttrs::construct(
+        2,
+        vec![
+            value_val,
+            forty_two_val.clone(),
+            name_val,
+            meaning_val.clone(),
+        ],
+    )
+    .expect("constructing K/V pair attrs should succeed");
+
+    match kv_attrs {
+        NixAttrs(AttrsRep::KV { name, value })
+            if name.to_str().unwrap() == meaning_val.to_str().unwrap()
+                || value.to_str().unwrap() == forty_two_val.to_str().unwrap() => {}
+
+        _ => panic!(
+            "K/V attribute set should use optimised representation, but got {:?}",
+            kv_attrs
+        ),
+    }
+}
+
+#[test]
+fn test_empty_attrs_iter() {
+    let attrs = NixAttrs::construct(0, vec![]).unwrap();
+    assert!(attrs.iter().next().is_none());
+}
+
+#[test]
+fn test_kv_attrs_iter() {
+    let name_val = Value::String("name".into());
+    let value_val = Value::String("value".into());
+    let meaning_val = Value::String("meaning".into());
+    let forty_two_val = Value::Integer(42);
+
+    let kv_attrs = NixAttrs::construct(
+        2,
+        vec![
+            value_val,
+            forty_two_val.clone(),
+            name_val,
+            meaning_val.clone(),
+        ],
+    )
+    .expect("constructing K/V pair attrs should succeed");
+
+    let mut iter = kv_attrs
+        .iter()
+        .collect::<Vec<_>>()
+        .into_iter()
+        .map(|(k, v)| (k, v));
+    let (k, v) = iter.next().unwrap();
+    assert!(k == *NAME_REF);
+    assert!(v.to_str().unwrap() == meaning_val.to_str().unwrap());
+    let (k, v) = iter.next().unwrap();
+    assert!(k == *VALUE_REF);
+    assert!(v.as_int().unwrap() == forty_two_val.as_int().unwrap());
+    assert!(iter.next().is_none());
+}
+
+#[test]
+fn test_map_attrs_iter() {
+    let attrs = NixAttrs::construct(
+        1,
+        vec![Value::String("key".into()), Value::String("value".into())],
+    )
+    .expect("simple attr construction should succeed");
+
+    let mut iter = attrs.iter().collect::<Vec<_>>().into_iter();
+    let (k, v) = iter.next().unwrap();
+    assert!(k == &NixString::from("key"));
+    assert!(v.to_str().unwrap().as_str() == "value");
+    assert!(iter.next().is_none());
+}
diff --git a/tvix/eval/src/value/builtin.rs b/tvix/eval/src/value/builtin.rs
new file mode 100644
index 000000000000..6d08ebf9506d
--- /dev/null
+++ b/tvix/eval/src/value/builtin.rs
@@ -0,0 +1,136 @@
+//! This module implements the runtime representation of a Nix
+//! builtin.
+//!
+//! Builtins are directly backed by Rust code operating on Nix values.
+
+use crate::vm::generators::Generator;
+
+use super::Value;
+
+use std::{
+    fmt::{Debug, Display},
+    rc::Rc,
+};
+
+/// Trait for closure types of builtins.
+///
+/// Builtins are expected to yield a generator which can be run by the VM to
+/// produce the final value.
+///
+/// Implementors should use the builtins-macros to create these functions
+/// instead of handling the argument-passing logic manually.
+pub trait BuiltinGen: Fn(Vec<Value>) -> Generator {}
+impl<F: Fn(Vec<Value>) -> Generator> BuiltinGen for F {}
+
+#[derive(Clone)]
+pub struct BuiltinRepr {
+    name: &'static str,
+    /// Optional documentation for the builtin.
+    documentation: Option<&'static str>,
+    arg_count: usize,
+
+    func: Rc<dyn BuiltinGen>,
+
+    /// Partially applied function arguments.
+    partials: Vec<Value>,
+}
+
+pub enum BuiltinResult {
+    /// Builtin was not ready to be called (arguments missing) and remains
+    /// partially applied.
+    Partial(Builtin),
+
+    /// Builtin was called and constructed a generator that the VM must run.
+    Called(&'static str, Generator),
+}
+
+/// Represents a single built-in function which directly executes Rust
+/// code that operates on a Nix value.
+///
+/// Builtins are the only functions in Nix that have varying arities
+/// (for example, `hasAttr` has an arity of 2, but `isAttrs` an arity
+/// of 1). To facilitate this generically, builtins expect to be
+/// called with a vector of Nix values corresponding to their
+/// arguments in order.
+///
+/// Partially applied builtins act similar to closures in that they
+/// "capture" the partially applied arguments, and are treated
+/// specially when printing their representation etc.
+#[derive(Clone)]
+pub struct Builtin(Box<BuiltinRepr>);
+
+impl From<BuiltinRepr> for Builtin {
+    fn from(value: BuiltinRepr) -> Self {
+        Builtin(Box::new(value))
+    }
+}
+
+impl Builtin {
+    pub fn new<F: BuiltinGen + 'static>(
+        name: &'static str,
+        documentation: Option<&'static str>,
+        arg_count: usize,
+        func: F,
+    ) -> Self {
+        BuiltinRepr {
+            name,
+            documentation,
+            arg_count,
+            func: Rc::new(func),
+            partials: vec![],
+        }
+        .into()
+    }
+
+    pub fn name(&self) -> &'static str {
+        self.0.name
+    }
+
+    pub fn documentation(&self) -> Option<&'static str> {
+        self.0.documentation
+    }
+
+    /// Apply an additional argument to the builtin. After this, [`call`] *must*
+    /// be called, otherwise it may leave the builtin in an incorrect state.
+    pub fn apply_arg(&mut self, arg: Value) {
+        self.0.partials.push(arg);
+
+        debug_assert!(
+            self.0.partials.len() <= self.0.arg_count,
+            "Tvix bug: pushed too many arguments to builtin"
+        );
+    }
+
+    /// Attempt to call a builtin, which will produce a generator if it is fully
+    /// applied or return the builtin if it is partially applied.
+    pub fn call(self) -> BuiltinResult {
+        if self.0.partials.len() == self.0.arg_count {
+            BuiltinResult::Called(self.0.name, (self.0.func)(self.0.partials))
+        } else {
+            BuiltinResult::Partial(self)
+        }
+    }
+}
+
+impl Debug for Builtin {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "builtin[{}]", self.0.name)
+    }
+}
+
+impl Display for Builtin {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if !self.0.partials.is_empty() {
+            f.write_str("<<primop-app>>")
+        } else {
+            f.write_str("<<primop>>")
+        }
+    }
+}
+
+/// Builtins are uniquely identified by their name
+impl PartialEq for Builtin {
+    fn eq(&self, other: &Self) -> bool {
+        self.0.name == other.0.name
+    }
+}
diff --git a/tvix/eval/src/value/function.rs b/tvix/eval/src/value/function.rs
new file mode 100644
index 000000000000..1a32795393bf
--- /dev/null
+++ b/tvix/eval/src/value/function.rs
@@ -0,0 +1,108 @@
+//! This module implements the runtime representation of functions.
+use std::{collections::HashMap, hash::Hash, rc::Rc};
+
+use codemap::Span;
+use smol_str::SmolStr;
+
+use crate::{chunk::Chunk, upvalues::Upvalues};
+
+use super::NixString;
+
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) struct Formals {
+    /// Map from argument name, to whether that argument is required
+    pub(crate) arguments: HashMap<NixString, bool>,
+
+    /// Do the formals of this function accept extra arguments
+    pub(crate) ellipsis: bool,
+
+    /// The span of the formals themselves, to use to emit errors
+    pub(crate) span: Span,
+}
+
+impl Formals {
+    /// Returns true if the given arg is a valid argument to these formals.
+    ///
+    /// This is true if it is either listed in the list of arguments, or the formals have an
+    /// ellipsis
+    pub(crate) fn contains<Q>(&self, arg: &Q) -> bool
+    where
+        Q: ?Sized + Hash + Eq,
+        NixString: std::borrow::Borrow<Q>,
+    {
+        self.ellipsis || self.arguments.contains_key(arg)
+    }
+}
+
+/// The opcodes for a thunk or closure, plus the number of
+/// non-executable opcodes which are allowed after an OpThunkClosure or
+/// OpThunkSuspended referencing it.  At runtime `Lambda` is usually wrapped
+/// in `Rc` to avoid copying the `Chunk` it holds (which can be
+/// quite large).
+///
+/// In order to correctly reproduce cppnix's "pointer equality"
+/// semantics it is important that we never clone a Lambda --
+/// use Rc<Lambda>::clone() instead.  This struct deliberately
+/// does not `derive(Clone)` in order to prevent this from being
+/// done accidentally.
+///
+#[derive(/* do not add Clone here */ Debug, Default)]
+pub struct Lambda {
+    pub(crate) chunk: Chunk,
+
+    /// Name of the function (equivalent to the name of the
+    /// identifier (e.g. a value in a let-expression or an attribute
+    /// set entry) it is located in).
+    pub(crate) name: Option<SmolStr>,
+
+    /// Number of upvalues which the code in this Lambda closes
+    /// over, and which need to be initialised at
+    /// runtime.  Information about the variables is emitted using
+    /// data-carrying opcodes (see [`OpCode::DataStackIdx`]).
+    pub(crate) upvalue_count: usize,
+    pub(crate) formals: Option<Formals>,
+}
+
+impl Lambda {
+    pub fn chunk(&mut self) -> &mut Chunk {
+        &mut self.chunk
+    }
+}
+
+///
+/// In order to correctly reproduce cppnix's "pointer equality"
+/// semantics it is important that we never clone a Lambda --
+/// use Rc<Lambda>::clone() instead.  This struct deliberately
+/// does not `derive(Clone)` in order to prevent this from being
+/// done accidentally.
+///
+#[derive(/* do not add Clone here */ Debug)]
+pub struct Closure {
+    pub lambda: Rc<Lambda>,
+    pub upvalues: Rc<Upvalues>,
+}
+
+impl Closure {
+    pub fn new(lambda: Rc<Lambda>) -> Self {
+        Self::new_with_upvalues(
+            Rc::new(Upvalues::with_capacity(lambda.upvalue_count)),
+            lambda,
+        )
+    }
+
+    pub fn new_with_upvalues(upvalues: Rc<Upvalues>, lambda: Rc<Lambda>) -> Self {
+        Closure { upvalues, lambda }
+    }
+
+    pub fn chunk(&self) -> &Chunk {
+        &self.lambda.chunk
+    }
+
+    pub fn lambda(&self) -> Rc<Lambda> {
+        self.lambda.clone()
+    }
+
+    pub fn upvalues(&self) -> Rc<Upvalues> {
+        self.upvalues.clone()
+    }
+}
diff --git a/tvix/eval/src/value/json.rs b/tvix/eval/src/value/json.rs
new file mode 100644
index 000000000000..33e16ebffcd0
--- /dev/null
+++ b/tvix/eval/src/value/json.rs
@@ -0,0 +1,84 @@
+/// Implementation of Value serialisation *to* JSON.
+///
+/// This can not be implemented through standard serde-derive methods,
+/// as there is internal Nix logic that must happen within the
+/// serialisation methods.
+use super::{CoercionKind, Value};
+use crate::generators::{self, GenCo};
+use crate::ErrorKind;
+
+use serde_json::value::to_value;
+use serde_json::Value as Json;
+use serde_json::{Map, Number}; // name clash with *our* `Value`
+
+impl Value {
+    pub(crate) async fn to_json(self, co: &GenCo) -> Result<Json, ErrorKind> {
+        let self_forced = generators::request_force(co, self).await;
+
+        let value = match self_forced {
+            Value::Null => Json::Null,
+            Value::Bool(b) => Json::Bool(b),
+            Value::Integer(i) => Json::Number(Number::from(i)),
+            Value::Float(f) => to_value(f)?,
+            Value::String(s) => Json::String(s.as_str().into()),
+
+            Value::Path(p) => {
+                let imported = generators::request_path_import(co, *p).await;
+                Json::String(imported.to_string_lossy().to_string())
+            }
+
+            Value::List(l) => {
+                let mut out = vec![];
+
+                for val in l.into_iter() {
+                    out.push(generators::request_to_json(co, val).await);
+                }
+
+                Json::Array(out)
+            }
+
+            Value::Attrs(attrs) => {
+                // Attribute sets with a callable `__toString` attribute
+                // serialise to the string-coerced version of the result of
+                // calling that.
+                if let Some(s) = attrs.try_to_string(&co, CoercionKind::Weak).await {
+                    return Ok(Json::String(s.as_str().to_string()));
+                }
+
+                // Attribute sets with an `outPath` attribute
+                // serialise to a JSON serialisation of that inner
+                // value (regardless of what it is!).
+                if let Some(out_path) = attrs.select("outPath") {
+                    return Ok(generators::request_to_json(co, out_path.clone()).await);
+                }
+
+                let mut out = Map::with_capacity(attrs.len());
+                for (name, value) in attrs.into_iter_sorted() {
+                    out.insert(
+                        name.as_str().to_string(),
+                        generators::request_to_json(co, value).await,
+                    );
+                }
+
+                Json::Object(out)
+            }
+
+            val @ Value::Closure(_)
+            | val @ Value::Thunk(_)
+            | val @ Value::Builtin(_)
+            | val @ Value::AttrNotFound
+            | val @ Value::Blueprint(_)
+            | val @ Value::DeferredUpvalue(_)
+            | val @ Value::UnresolvedPath(_)
+            | val @ Value::Json(_) => return Err(ErrorKind::NotSerialisableToJson(val.type_of())),
+        };
+
+        Ok(value)
+    }
+
+    /// Generator version of the above, which wraps responses in
+    /// Value::Json.
+    pub(crate) async fn to_json_generator(self, co: GenCo) -> Result<Value, ErrorKind> {
+        Ok(Value::Json(self.to_json(&co).await?))
+    }
+}
diff --git a/tvix/eval/src/value/list.rs b/tvix/eval/src/value/list.rs
new file mode 100644
index 000000000000..cfaefff82195
--- /dev/null
+++ b/tvix/eval/src/value/list.rs
@@ -0,0 +1,152 @@
+//! This module implements Nix lists.
+use std::ops::Index;
+use std::rc::Rc;
+
+use imbl::{vector, Vector};
+
+use serde::Deserialize;
+
+use crate::generators;
+use crate::generators::GenCo;
+use crate::AddContext;
+use crate::ErrorKind;
+
+use super::thunk::ThunkSet;
+use super::TotalDisplay;
+use super::Value;
+
+#[repr(transparent)]
+#[derive(Clone, Debug, Deserialize)]
+pub struct NixList(Rc<Vector<Value>>);
+
+impl TotalDisplay for NixList {
+    fn total_fmt(&self, f: &mut std::fmt::Formatter<'_>, set: &mut ThunkSet) -> std::fmt::Result {
+        f.write_str("[ ")?;
+
+        for v in self {
+            v.total_fmt(f, set)?;
+            f.write_str(" ")?;
+        }
+
+        f.write_str("]")
+    }
+}
+
+impl From<Vector<Value>> for NixList {
+    fn from(vs: Vector<Value>) -> Self {
+        Self(Rc::new(vs))
+    }
+}
+
+impl NixList {
+    pub fn len(&self) -> usize {
+        self.0.len()
+    }
+
+    pub fn get(&self, i: usize) -> Option<&Value> {
+        self.0.get(i)
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+
+    pub fn construct(count: usize, stack_slice: Vec<Value>) -> Self {
+        debug_assert!(
+            count == stack_slice.len(),
+            "NixList::construct called with count == {}, but slice.len() == {}",
+            count,
+            stack_slice.len(),
+        );
+
+        NixList(Rc::new(Vector::from_iter(stack_slice.into_iter())))
+    }
+
+    pub fn iter(&self) -> vector::Iter<Value> {
+        self.0.iter()
+    }
+
+    pub fn ptr_eq(&self, other: &Self) -> bool {
+        Rc::ptr_eq(&self.0, &other.0)
+    }
+
+    pub fn into_inner(self) -> Vector<Value> {
+        Rc::try_unwrap(self.0).unwrap_or_else(|rc| (*rc).clone())
+    }
+
+    #[deprecated(note = "callers should avoid constructing from Vec")]
+    pub fn from_vec(vs: Vec<Value>) -> Self {
+        Self(Rc::new(Vector::from_iter(vs.into_iter())))
+    }
+
+    /// Asynchronous sorting algorithm in which the comparator can make use of
+    /// VM requests (required as `builtins.sort` uses comparators written in
+    /// Nix).
+    ///
+    /// This is a simple, optimised bubble sort implementation. The choice of
+    /// algorithm is constrained by the comparator in Nix not being able to
+    /// yield equality, and us being unable to use the standard library
+    /// implementation of sorting (which is a lot longer, but a lot more
+    /// efficient) here.
+    // TODO(amjoseph): Investigate potential impl in Nix code, or Tvix bytecode.
+    pub async fn sort_by(self, co: &GenCo, cmp: Value) -> Result<Self, ErrorKind> {
+        let mut len = self.len();
+        let mut data = self.into_inner();
+
+        loop {
+            let mut new_len = 0;
+            for i in 1..len {
+                if generators::request_force(
+                    co,
+                    generators::request_call_with(
+                        co,
+                        cmp.clone(),
+                        [data[i].clone(), data[i - 1].clone()],
+                    )
+                    .await,
+                )
+                .await
+                .as_bool()
+                .context("evaluating comparator in `builtins.sort`")?
+                {
+                    data.swap(i, i - 1);
+                    new_len = i;
+                }
+            }
+
+            if new_len == 0 {
+                break;
+            }
+
+            len = new_len;
+        }
+
+        Ok(data.into())
+    }
+}
+
+impl IntoIterator for NixList {
+    type Item = Value;
+    type IntoIter = imbl::vector::ConsumingIter<Value>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.into_inner().into_iter()
+    }
+}
+
+impl<'a> IntoIterator for &'a NixList {
+    type Item = &'a Value;
+    type IntoIter = imbl::vector::Iter<'a, Value>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.iter()
+    }
+}
+
+impl Index<usize> for NixList {
+    type Output = Value;
+
+    fn index(&self, index: usize) -> &Self::Output {
+        &self.0[index]
+    }
+}
diff --git a/tvix/eval/src/value/mod.rs b/tvix/eval/src/value/mod.rs
new file mode 100644
index 000000000000..77d40b0e3906
--- /dev/null
+++ b/tvix/eval/src/value/mod.rs
@@ -0,0 +1,842 @@
+//! This module implements the backing representation of runtime
+//! values in the Nix language.
+use std::cmp::Ordering;
+use std::fmt::Display;
+use std::num::{NonZeroI32, NonZeroUsize};
+use std::path::PathBuf;
+use std::rc::Rc;
+
+use lexical_core::format::CXX_LITERAL;
+use serde::Deserialize;
+
+#[cfg(feature = "arbitrary")]
+mod arbitrary;
+mod attrs;
+mod builtin;
+mod function;
+mod json;
+mod list;
+mod path;
+mod string;
+mod thunk;
+
+use crate::errors::ErrorKind;
+use crate::opcode::StackIdx;
+use crate::spans::LightSpan;
+use crate::vm::generators::{self, GenCo};
+use crate::AddContext;
+pub use attrs::NixAttrs;
+pub use builtin::{Builtin, BuiltinResult};
+pub(crate) use function::Formals;
+pub use function::{Closure, Lambda};
+pub use list::NixList;
+pub use path::canon_path;
+pub use string::NixString;
+pub use thunk::Thunk;
+
+pub use self::thunk::{SharedThunkSet, ThunkSet};
+
+use lazy_static::lazy_static;
+
+#[warn(variant_size_differences)]
+#[derive(Clone, Debug, Deserialize)]
+#[serde(untagged)]
+pub enum Value {
+    Null,
+    Bool(bool),
+    Integer(i64),
+    Float(f64),
+    String(NixString),
+
+    #[serde(skip)]
+    Path(Box<PathBuf>),
+    Attrs(Box<NixAttrs>),
+    List(NixList),
+
+    #[serde(skip)]
+    Closure(Rc<Closure>), // must use Rc<Closure> here in order to get proper pointer equality
+
+    #[serde(skip)]
+    Builtin(Builtin),
+
+    // Internal values that, while they technically exist at runtime,
+    // are never returned to or created directly by users.
+    #[serde(skip_deserializing)]
+    Thunk(Thunk),
+
+    // See [`compiler::compile_select_or()`] for explanation
+    #[serde(skip)]
+    AttrNotFound,
+
+    // this can only occur in Chunk::Constants and nowhere else
+    #[serde(skip)]
+    Blueprint(Rc<Lambda>),
+
+    #[serde(skip)]
+    DeferredUpvalue(StackIdx),
+    #[serde(skip)]
+    UnresolvedPath(Box<PathBuf>),
+    #[serde(skip)]
+    Json(serde_json::Value),
+}
+
+lazy_static! {
+    static ref WRITE_FLOAT_OPTIONS: lexical_core::WriteFloatOptions =
+        lexical_core::WriteFloatOptionsBuilder::new()
+            .trim_floats(true)
+            .round_mode(lexical_core::write_float_options::RoundMode::Round)
+            .positive_exponent_break(Some(NonZeroI32::new(5).unwrap()))
+            .max_significant_digits(Some(NonZeroUsize::new(6).unwrap()))
+            .build()
+            .unwrap();
+}
+
+// Helper macros to generate the to_*/as_* macros while accounting for
+// thunks.
+
+/// Generate an `as_*` method returning a reference to the expected
+/// type, or a type error. This only works for types that implement
+/// `Copy`, as returning a reference to an inner thunk value is not
+/// possible.
+
+/// Generate an `as_*/to_*` accessor method that returns either the
+/// expected type, or a type error.
+macro_rules! gen_cast {
+    ( $name:ident, $type:ty, $expected:expr, $variant:pat, $result:expr ) => {
+        pub fn $name(&self) -> Result<$type, ErrorKind> {
+            match self {
+                $variant => Ok($result),
+                Value::Thunk(thunk) => Self::$name(&thunk.value()),
+                other => Err(type_error($expected, &other)),
+            }
+        }
+    };
+}
+
+/// Generate an `as_*_mut/to_*_mut` accessor method that returns either the
+/// expected type, or a type error.
+macro_rules! gen_cast_mut {
+    ( $name:ident, $type:ty, $expected:expr, $variant:ident) => {
+        pub fn $name(&mut self) -> Result<&mut $type, ErrorKind> {
+            match self {
+                Value::$variant(x) => Ok(x),
+                other => Err(type_error($expected, &other)),
+            }
+        }
+    };
+}
+
+/// Generate an `is_*` type-checking method.
+macro_rules! gen_is {
+    ( $name:ident, $variant:pat ) => {
+        pub fn $name(&self) -> bool {
+            match self {
+                $variant => true,
+                Value::Thunk(thunk) => Self::$name(&thunk.value()),
+                _ => false,
+            }
+        }
+    };
+}
+
+/// Describes what input types are allowed when coercing a `Value` to a string
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum CoercionKind {
+    /// Only coerce already "stringly" types like strings and paths, but also
+    /// coerce sets that have a `__toString` attribute. Equivalent to
+    /// `!coerceMore` in C++ Nix.
+    Weak,
+    /// Coerce all value types included by `Weak`, but also coerce `null`,
+    /// booleans, integers, floats and lists of coercible types. Equivalent to
+    /// `coerceMore` in C++ Nix.
+    Strong,
+}
+
+impl<T> From<T> for Value
+where
+    T: Into<NixString>,
+{
+    fn from(t: T) -> Self {
+        Self::String(t.into())
+    }
+}
+
+/// Constructors
+impl Value {
+    /// Construct a [`Value::Attrs`] from a [`NixAttrs`].
+    pub fn attrs(attrs: NixAttrs) -> Self {
+        Self::Attrs(Box::new(attrs))
+    }
+}
+
+/// Controls what kind of by-pointer equality comparison is allowed.
+///
+/// See `//tvix/docs/value-pointer-equality.md` for details.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum PointerEquality {
+    /// Pointer equality not allowed at all.
+    ForbidAll,
+
+    /// Pointer equality comparisons only allowed for nested values.
+    AllowNested,
+
+    /// Pointer equality comparisons are allowed in all contexts.
+    AllowAll,
+}
+
+impl Value {
+    // TODO(amjoseph): de-asyncify this (when called directly by the VM)
+    /// Deeply forces a value, traversing e.g. lists and attribute sets and forcing
+    /// their contents, too.
+    ///
+    /// This is a generator function.
+    pub(super) async fn deep_force(
+        self,
+        co: GenCo,
+        thunk_set: SharedThunkSet,
+    ) -> Result<Value, ErrorKind> {
+        // Get rid of any top-level thunks, and bail out of self-recursive
+        // thunks.
+        let value = if let Value::Thunk(ref t) = &self {
+            if !thunk_set.insert(t) {
+                return Ok(self);
+            }
+            generators::request_force(&co, self).await
+        } else {
+            self
+        };
+
+        match &value {
+            // Short-circuit on already evaluated values, or fail on internal values.
+            Value::Null
+            | Value::Bool(_)
+            | Value::Integer(_)
+            | Value::Float(_)
+            | Value::String(_)
+            | Value::Path(_)
+            | Value::Closure(_)
+            | Value::Builtin(_) => return Ok(value),
+
+            Value::List(list) => {
+                for val in list {
+                    generators::request_deep_force(&co, val.clone(), thunk_set.clone()).await;
+                }
+            }
+
+            Value::Attrs(attrs) => {
+                for (_, val) in attrs.iter() {
+                    generators::request_deep_force(&co, val.clone(), thunk_set.clone()).await;
+                }
+            }
+
+            Value::Thunk(_) => panic!("Tvix bug: force_value() returned a thunk"),
+
+            Value::AttrNotFound
+            | Value::Blueprint(_)
+            | Value::DeferredUpvalue(_)
+            | Value::UnresolvedPath(_)
+            | Value::Json(_) => panic!(
+                "Tvix bug: internal value left on stack: {}",
+                value.type_of()
+            ),
+        };
+
+        Ok(value)
+    }
+
+    // TODO(amjoseph): de-asyncify this (when called directly by the VM)
+    /// Coerce a `Value` to a string. See `CoercionKind` for a rundown of what
+    /// input types are accepted under what circumstances.
+    pub async fn coerce_to_string(self, co: GenCo, kind: CoercionKind) -> Result<Value, ErrorKind> {
+        let value = generators::request_force(&co, self).await;
+
+        match (value, kind) {
+            // coercions that are always done
+            tuple @ (Value::String(_), _) => Ok(tuple.0),
+
+            // TODO(sterni): Think about proper encoding handling here. This needs
+            // general consideration anyways, since one current discrepancy between
+            // C++ Nix and Tvix is that the former's strings are arbitrary byte
+            // sequences without NUL bytes, whereas Tvix only allows valid
+            // Unicode. See also b/189.
+            (Value::Path(p), _) => {
+                // TODO(tazjin): there are cases where coerce_to_string does not import
+                let imported = generators::request_path_import(&co, *p).await;
+                Ok(imported.to_string_lossy().into_owned().into())
+            }
+
+            // Attribute sets can be converted to strings if they either have an
+            // `__toString` attribute which holds a function that receives the
+            // set itself or an `outPath` attribute which should be a string.
+            // `__toString` is preferred.
+            (Value::Attrs(attrs), kind) => {
+                if let Some(s) = attrs.try_to_string(&co, kind).await {
+                    return Ok(Value::String(s));
+                }
+
+                if let Some(out_path) = attrs.select("outPath") {
+                    let s = generators::request_string_coerce(&co, out_path.clone(), kind).await;
+                    return Ok(Value::String(s));
+                }
+
+                Err(ErrorKind::NotCoercibleToString { from: "set", kind })
+            }
+
+            // strong coercions
+            (Value::Null, CoercionKind::Strong) | (Value::Bool(false), CoercionKind::Strong) => {
+                Ok("".into())
+            }
+            (Value::Bool(true), CoercionKind::Strong) => Ok("1".into()),
+
+            (Value::Integer(i), CoercionKind::Strong) => Ok(format!("{i}").into()),
+            (Value::Float(f), CoercionKind::Strong) => {
+                // contrary to normal Display, coercing a float to a string will
+                // result in unconditional 6 decimal places
+                Ok(format!("{:.6}", f).into())
+            }
+
+            // Lists are coerced by coercing their elements and interspersing spaces
+            (Value::List(list), CoercionKind::Strong) => {
+                let mut out = String::new();
+
+                for (idx, elem) in list.into_iter().enumerate() {
+                    if idx > 0 {
+                        out.push(' ');
+                    }
+
+                    let s = generators::request_string_coerce(&co, elem, kind).await;
+                    out.push_str(s.as_str());
+                }
+
+                Ok(Value::String(out.into()))
+            }
+
+            (Value::Thunk(_), _) => panic!("Tvix bug: force returned unforced thunk"),
+
+            val @ (Value::Closure(_), _)
+            | val @ (Value::Builtin(_), _)
+            | val @ (Value::Null, _)
+            | val @ (Value::Bool(_), _)
+            | val @ (Value::Integer(_), _)
+            | val @ (Value::Float(_), _)
+            | val @ (Value::List(_), _) => Err(ErrorKind::NotCoercibleToString {
+                from: val.0.type_of(),
+                kind,
+            }),
+
+            (Value::AttrNotFound, _)
+            | (Value::Blueprint(_), _)
+            | (Value::DeferredUpvalue(_), _)
+            | (Value::UnresolvedPath(_), _)
+            | (Value::Json(_), _) => {
+                panic!("tvix bug: .coerce_to_string() called on internal value")
+            }
+        }
+    }
+
+    // TODO(amjoseph): de-asyncify this (when called directly by the VM)
+    /// Compare two Nix values for equality, forcing nested parts of the structure
+    /// as needed.
+    ///
+    /// This comparison needs to be invoked for nested values (e.g. in lists and
+    /// attribute sets) as well, which is done by suspending and asking the VM to
+    /// perform the nested comparison.
+    ///
+    /// The `top_level` parameter controls whether this invocation is the top-level
+    /// comparison, or a nested value comparison. See
+    /// `//tvix/docs/value-pointer-equality.md`
+    pub(crate) async fn nix_eq(
+        self,
+        other: Value,
+        co: GenCo,
+        ptr_eq: PointerEquality,
+    ) -> Result<Value, ErrorKind> {
+        let a = match self {
+            Value::Thunk(ref thunk) => {
+                // If both values are thunks, and thunk comparisons are allowed by
+                // pointer, do that and move on.
+                if ptr_eq == PointerEquality::AllowAll {
+                    if let Value::Thunk(t1) = &other {
+                        if t1.ptr_eq(thunk) {
+                            return Ok(Value::Bool(true));
+                        }
+                    }
+                };
+
+                generators::request_force(&co, self).await
+            }
+
+            _ => self,
+        };
+
+        let b = match other {
+            Value::Thunk(_) => generators::request_force(&co, other).await,
+            _ => other,
+        };
+
+        debug_assert!(!matches!(a, Value::Thunk(_)));
+        debug_assert!(!matches!(b, Value::Thunk(_)));
+
+        let result = match (a, b) {
+            // Trivial comparisons
+            (Value::Null, Value::Null) => true,
+            (Value::Bool(b1), Value::Bool(b2)) => b1 == b2,
+            (Value::String(s1), Value::String(s2)) => s1 == s2,
+            (Value::Path(p1), Value::Path(p2)) => p1 == p2,
+
+            // Numerical comparisons (they work between float & int)
+            (Value::Integer(i1), Value::Integer(i2)) => i1 == i2,
+            (Value::Integer(i), Value::Float(f)) => i as f64 == f,
+            (Value::Float(f1), Value::Float(f2)) => f1 == f2,
+            (Value::Float(f), Value::Integer(i)) => i as f64 == f,
+
+            // List comparisons
+            (Value::List(l1), Value::List(l2)) => {
+                if ptr_eq >= PointerEquality::AllowNested && l1.ptr_eq(&l2) {
+                    return Ok(Value::Bool(true));
+                }
+
+                if l1.len() != l2.len() {
+                    return Ok(Value::Bool(false));
+                }
+
+                for (vi1, vi2) in l1.into_iter().zip(l2.into_iter()) {
+                    if !generators::check_equality(
+                        &co,
+                        vi1,
+                        vi2,
+                        std::cmp::max(ptr_eq, PointerEquality::AllowNested),
+                    )
+                    .await?
+                    {
+                        return Ok(Value::Bool(false));
+                    }
+                }
+
+                true
+            }
+
+            (_, Value::List(_)) | (Value::List(_), _) => false,
+
+            // Attribute set comparisons
+            (Value::Attrs(a1), Value::Attrs(a2)) => {
+                if ptr_eq >= PointerEquality::AllowNested && a1.ptr_eq(&a2) {
+                    return Ok(Value::Bool(true));
+                }
+
+                // Special-case for derivation comparisons: If both attribute sets
+                // have `type = derivation`, compare them by `outPath`.
+                match (a1.select("type"), a2.select("type")) {
+                    (Some(v1), Some(v2)) => {
+                        let s1 = generators::request_force(&co, v1.clone()).await.to_str();
+                        let s2 = generators::request_force(&co, v2.clone()).await.to_str();
+
+                        if let (Ok(s1), Ok(s2)) = (s1, s2) {
+                            if s1.as_str() == "derivation" && s2.as_str() == "derivation" {
+                                // TODO(tazjin): are the outPaths really required,
+                                // or should it fall through?
+                                let out1 = a1
+                                    .select_required("outPath")
+                                    .context("comparing derivations")?
+                                    .clone();
+
+                                let out2 = a2
+                                    .select_required("outPath")
+                                    .context("comparing derivations")?
+                                    .clone();
+
+                                let result = generators::request_force(&co, out1.clone())
+                                    .await
+                                    .to_str()?
+                                    == generators::request_force(&co, out2.clone())
+                                        .await
+                                        .to_str()?;
+                                return Ok(Value::Bool(result));
+                            }
+                        }
+                    }
+                    _ => {}
+                };
+
+                if a1.len() != a2.len() {
+                    return Ok(Value::Bool(false));
+                }
+
+                let iter1 = a1.into_iter_sorted();
+                let iter2 = a2.into_iter_sorted();
+
+                for ((k1, v1), (k2, v2)) in iter1.zip(iter2) {
+                    if k1 != k2 {
+                        return Ok(Value::Bool(false));
+                    }
+
+                    if !generators::check_equality(
+                        &co,
+                        v1,
+                        v2,
+                        std::cmp::max(ptr_eq, PointerEquality::AllowNested),
+                    )
+                    .await?
+                    {
+                        return Ok(Value::Bool(false));
+                    }
+                }
+
+                true
+            }
+
+            (Value::Attrs(_), _) | (_, Value::Attrs(_)) => false,
+
+            (Value::Closure(c1), Value::Closure(c2)) if ptr_eq >= PointerEquality::AllowNested => {
+                Rc::ptr_eq(&c1, &c2)
+            }
+
+            // Everything else is either incomparable (e.g. internal types) or
+            // false.
+            _ => false,
+        };
+
+        Ok(Value::Bool(result))
+    }
+
+    pub fn type_of(&self) -> &'static str {
+        match self {
+            Value::Null => "null",
+            Value::Bool(_) => "bool",
+            Value::Integer(_) => "int",
+            Value::Float(_) => "float",
+            Value::String(_) => "string",
+            Value::Path(_) => "path",
+            Value::Attrs(_) => "set",
+            Value::List(_) => "list",
+            Value::Closure(_) | Value::Builtin(_) => "lambda",
+
+            // Internal types. Note: These are only elaborated here
+            // because it makes debugging easier. If a user ever sees
+            // any of these strings, it's a bug.
+            Value::Thunk(_) => "internal[thunk]",
+            Value::AttrNotFound => "internal[attr_not_found]",
+            Value::Blueprint(_) => "internal[blueprint]",
+            Value::DeferredUpvalue(_) => "internal[deferred_upvalue]",
+            Value::UnresolvedPath(_) => "internal[unresolved_path]",
+            Value::Json(_) => "internal[json]",
+        }
+    }
+
+    gen_cast!(as_bool, bool, "bool", Value::Bool(b), *b);
+    gen_cast!(as_int, i64, "int", Value::Integer(x), *x);
+    gen_cast!(as_float, f64, "float", Value::Float(x), *x);
+    gen_cast!(to_str, NixString, "string", Value::String(s), s.clone());
+    gen_cast!(to_attrs, Box<NixAttrs>, "set", Value::Attrs(a), a.clone());
+    gen_cast!(to_list, NixList, "list", Value::List(l), l.clone());
+    gen_cast!(
+        as_closure,
+        Rc<Closure>,
+        "lambda",
+        Value::Closure(c),
+        c.clone()
+    );
+
+    gen_cast_mut!(as_list_mut, NixList, "list", List);
+
+    gen_is!(is_path, Value::Path(_));
+    gen_is!(is_number, Value::Integer(_) | Value::Float(_));
+    gen_is!(is_bool, Value::Bool(_));
+
+    // TODO(amjoseph): de-asyncify this (when called directly by the VM)
+    /// Compare `self` against other using (fallible) Nix ordering semantics.
+    ///
+    /// Note that as this returns an `Option<Ordering>` it can not directly be
+    /// used as a generator function in the VM. The exact use depends on the
+    /// callsite, as the meaning is interpreted in different ways e.g. based on
+    /// the comparison operator used.
+    ///
+    /// The function is intended to be used from within other generator
+    /// functions or `gen!` blocks.
+    pub async fn nix_cmp_ordering(
+        self,
+        other: Self,
+        co: GenCo,
+    ) -> Result<Option<Ordering>, ErrorKind> {
+        Self::nix_cmp_ordering_(self, other, co).await
+    }
+
+    async fn nix_cmp_ordering_(
+        mut myself: Self,
+        mut other: Self,
+        co: GenCo,
+    ) -> Result<Option<Ordering>, ErrorKind> {
+        'outer: loop {
+            match (myself, other) {
+                // same types
+                (Value::Integer(i1), Value::Integer(i2)) => return Ok(i1.partial_cmp(&i2)),
+                (Value::Float(f1), Value::Float(f2)) => return Ok(f1.partial_cmp(&f2)),
+                (Value::String(s1), Value::String(s2)) => return Ok(s1.partial_cmp(&s2)),
+                (Value::List(l1), Value::List(l2)) => {
+                    for i in 0.. {
+                        if i == l2.len() {
+                            return Ok(Some(Ordering::Greater));
+                        } else if i == l1.len() {
+                            return Ok(Some(Ordering::Less));
+                        } else if !generators::check_equality(
+                            &co,
+                            l1[i].clone(),
+                            l2[i].clone(),
+                            PointerEquality::AllowAll,
+                        )
+                        .await?
+                        {
+                            // TODO: do we need to control `top_level` here?
+                            myself = generators::request_force(&co, l1[i].clone()).await;
+                            other = generators::request_force(&co, l2[i].clone()).await;
+                            continue 'outer;
+                        }
+                    }
+
+                    unreachable!()
+                }
+
+                // different types
+                (Value::Integer(i1), Value::Float(f2)) => return Ok((i1 as f64).partial_cmp(&f2)),
+                (Value::Float(f1), Value::Integer(i2)) => return Ok(f1.partial_cmp(&(i2 as f64))),
+
+                // unsupported types
+                (lhs, rhs) => {
+                    return Err(ErrorKind::Incomparable {
+                        lhs: lhs.type_of(),
+                        rhs: rhs.type_of(),
+                    })
+                }
+            }
+        }
+    }
+
+    // TODO(amjoseph): de-asyncify this (when called directly by the VM)
+    pub async fn force(self, co: GenCo, span: LightSpan) -> Result<Value, ErrorKind> {
+        if let Value::Thunk(thunk) = self {
+            return thunk.force(co, span).await;
+        }
+
+        Ok(self)
+    }
+
+    /// Explain a value in a human-readable way, e.g. by presenting
+    /// the docstrings of functions if present.
+    pub fn explain(&self) -> String {
+        match self {
+            Value::Null => "the 'null' value".into(),
+            Value::Bool(b) => format!("the boolean value '{}'", b),
+            Value::Integer(i) => format!("the integer '{}'", i),
+            Value::Float(f) => format!("the float '{}'", f),
+            Value::String(s) => format!("the string '{}'", s),
+            Value::Path(p) => format!("the path '{}'", p.to_string_lossy()),
+            Value::Attrs(attrs) => format!("a {}-item attribute set", attrs.len()),
+            Value::List(list) => format!("a {}-item list", list.len()),
+
+            Value::Closure(f) => {
+                if let Some(name) = &f.lambda.name {
+                    format!("the user-defined Nix function '{}'", name)
+                } else {
+                    "a user-defined Nix function".to_string()
+                }
+            }
+
+            Value::Builtin(b) => {
+                let mut out = format!("the builtin function '{}'", b.name());
+                if let Some(docs) = b.documentation() {
+                    out.push_str("\n\n");
+                    out.push_str(docs);
+                }
+                out
+            }
+
+            // TODO: handle suspended thunks with a different explanation instead of panicking
+            Value::Thunk(t) => t.value().explain(),
+
+            Value::AttrNotFound
+            | Value::Blueprint(_)
+            | Value::DeferredUpvalue(_)
+            | Value::UnresolvedPath(_)
+            | Value::Json(_) => "an internal Tvix evaluator value".into(),
+        }
+    }
+}
+
+trait TotalDisplay {
+    fn total_fmt(&self, f: &mut std::fmt::Formatter<'_>, set: &mut ThunkSet) -> std::fmt::Result;
+}
+
+impl Display for Value {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.total_fmt(f, &mut Default::default())
+    }
+}
+
+/// Emulates the C++-Nix style formatting of floats, which diverges
+/// significantly from Rust's native float formatting.
+fn total_fmt_float<F: std::fmt::Write>(num: f64, mut f: F) -> std::fmt::Result {
+    let mut buf = [b'0'; lexical_core::BUFFER_SIZE];
+    let mut s = lexical_core::write_with_options::<f64, { CXX_LITERAL }>(
+        num,
+        &mut buf,
+        &WRITE_FLOAT_OPTIONS,
+    );
+
+    // apply some postprocessing on the buffer. If scientific
+    // notation is used (we see an `e`), and the next character is
+    // a digit, add the missing `+` sign.)
+    let mut new_s = Vec::with_capacity(s.len());
+
+    if s.contains(&b'e') {
+        for (i, c) in s.iter().enumerate() {
+            // encountered `e`
+            if c == &b'e' {
+                // next character is a digit (so no negative exponent)
+                if s.len() > i && s[i + 1].is_ascii_digit() {
+                    // copy everything from the start up to (including) the e
+                    new_s.extend_from_slice(&s[0..=i]);
+                    // add the missing '+'
+                    new_s.push(b'+');
+                    // check for the remaining characters.
+                    // If it's only one, we need to prepend a trailing zero
+                    if s.len() == i + 2 {
+                        new_s.push(b'0');
+                    }
+                    new_s.extend_from_slice(&s[i + 1..]);
+                    break;
+                }
+            }
+        }
+
+        // if we modified the scientific notation, flip the reference
+        if !new_s.is_empty() {
+            s = &mut new_s
+        }
+    }
+    // else, if this is not scientific notation, and there's a
+    // decimal point, make sure we really drop trailing zeroes.
+    // In some cases, lexical_core doesn't.
+    else if s.contains(&b'.') {
+        for (i, c) in s.iter().enumerate() {
+            // at `.``
+            if c == &b'.' {
+                // trim zeroes from the right side.
+                let frac = String::from_utf8_lossy(&s[i + 1..]);
+                let frac_no_trailing_zeroes = frac.trim_end_matches('0');
+
+                if frac.len() != frac_no_trailing_zeroes.len() {
+                    // we managed to strip something, construct new_s
+                    if frac_no_trailing_zeroes.is_empty() {
+                        // if frac_no_trailing_zeroes is empty, the fractional part was all zeroes, so we can drop the decimal point as well
+                        new_s.extend_from_slice(&s[0..=i - 1]);
+                    } else {
+                        // else, assemble the rest of the string
+                        new_s.extend_from_slice(&s[0..=i]);
+                        new_s.extend_from_slice(frac_no_trailing_zeroes.as_bytes());
+                    }
+
+                    // flip the reference
+                    s = &mut new_s;
+                    break;
+                }
+            }
+        }
+    }
+
+    write!(f, "{}", format!("{}", String::from_utf8_lossy(s)))
+}
+
+impl TotalDisplay for Value {
+    fn total_fmt(&self, f: &mut std::fmt::Formatter<'_>, set: &mut ThunkSet) -> std::fmt::Result {
+        match self {
+            Value::Null => f.write_str("null"),
+            Value::Bool(true) => f.write_str("true"),
+            Value::Bool(false) => f.write_str("false"),
+            Value::Integer(num) => write!(f, "{}", num),
+            Value::String(s) => s.fmt(f),
+            Value::Path(p) => p.display().fmt(f),
+            Value::Attrs(attrs) => attrs.total_fmt(f, set),
+            Value::List(list) => list.total_fmt(f, set),
+            Value::Closure(_) => f.write_str("lambda"), // TODO: print position
+            Value::Builtin(builtin) => builtin.fmt(f),
+
+            // Nix prints floats with a maximum precision of 5 digits
+            // only. Except when it decides to use scientific notation
+            // (with a + after the `e`, and zero-padded to 0 digits)
+            Value::Float(num) => total_fmt_float(*num, f),
+
+            // internal types
+            Value::AttrNotFound => f.write_str("internal[not found]"),
+            Value::Blueprint(_) => f.write_str("internal[blueprint]"),
+            Value::DeferredUpvalue(_) => f.write_str("internal[deferred_upvalue]"),
+            Value::UnresolvedPath(_) => f.write_str("internal[unresolved_path]"),
+            Value::Json(_) => f.write_str("internal[json]"),
+
+            // Delegate thunk display to the type, as it must handle
+            // the case of already evaluated or cyclic thunks.
+            Value::Thunk(t) => t.total_fmt(f, set),
+        }
+    }
+}
+
+impl From<bool> for Value {
+    fn from(b: bool) -> Self {
+        Value::Bool(b)
+    }
+}
+
+impl From<i64> for Value {
+    fn from(i: i64) -> Self {
+        Self::Integer(i)
+    }
+}
+
+impl From<f64> for Value {
+    fn from(i: f64) -> Self {
+        Self::Float(i)
+    }
+}
+
+impl From<PathBuf> for Value {
+    fn from(path: PathBuf) -> Self {
+        Self::Path(Box::new(path))
+    }
+}
+
+fn type_error(expected: &'static str, actual: &Value) -> ErrorKind {
+    ErrorKind::TypeError {
+        expected,
+        actual: actual.type_of(),
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    mod floats {
+        use crate::value::total_fmt_float;
+
+        #[test]
+        fn format_float() {
+            let ff = vec![
+                (0f64, "0"),
+                (1.0f64, "1"),
+                (-0.01, "-0.01"),
+                (5e+22, "5e+22"),
+                (1e6, "1e+06"),
+                (-2E-2, "-0.02"),
+                (6.626e-34, "6.626e-34"),
+                (9_224_617.445_991_227, "9.22462e+06"),
+            ];
+            for (n, expected) in ff.iter() {
+                let mut buf = String::new();
+                let res = total_fmt_float(*n, &mut buf);
+                assert!(res.is_ok());
+                assert_eq!(
+                    expected, &buf,
+                    "{} should be formatted as {}, but got {}",
+                    n, expected, &buf
+                );
+            }
+        }
+    }
+}
diff --git a/tvix/eval/src/value/path.rs b/tvix/eval/src/value/path.rs
new file mode 100644
index 000000000000..ad526a8746f8
--- /dev/null
+++ b/tvix/eval/src/value/path.rs
@@ -0,0 +1,14 @@
+use path_clean::PathClean;
+use std::path::PathBuf;
+
+/// This function should match the behavior of canonPath() in
+/// src/libutil/util.cc of cppnix.  Currently it does not match that
+/// behavior; it uses the `path_clean` library which is based on the
+/// Go standard library
+///
+/// TODO: make this match the behavior of cppnix
+/// TODO: write tests for this
+
+pub fn canon_path(path: PathBuf) -> PathBuf {
+    path.clean()
+}
diff --git a/tvix/eval/src/value/string.rs b/tvix/eval/src/value/string.rs
new file mode 100644
index 000000000000..7144ca360d15
--- /dev/null
+++ b/tvix/eval/src/value/string.rs
@@ -0,0 +1,262 @@
+//! This module implements Nix language strings.
+//!
+//! Nix language strings never need to be modified on the language
+//! level, allowing us to shave off some memory overhead and only
+//! paying the cost when creating new strings.
+use rnix::ast;
+use std::ffi::OsStr;
+use std::hash::Hash;
+use std::ops::Deref;
+use std::path::Path;
+use std::{borrow::Cow, fmt::Display, str::Chars};
+
+use serde::de::{Deserializer, Visitor};
+use serde::{Deserialize, Serialize};
+
+#[repr(transparent)]
+#[derive(Clone, Debug, Serialize)]
+pub struct NixString(Box<str>);
+
+impl PartialEq for NixString {
+    fn eq(&self, other: &Self) -> bool {
+        self.as_str() == other.as_str()
+    }
+}
+
+impl Eq for NixString {}
+
+impl PartialOrd for NixString {
+    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+        self.as_str().partial_cmp(other.as_str())
+    }
+}
+
+impl Ord for NixString {
+    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+        self.as_str().cmp(other.as_str())
+    }
+}
+
+impl From<&str> for NixString {
+    fn from(s: &str) -> Self {
+        NixString(Box::from(s))
+    }
+}
+
+impl From<String> for NixString {
+    fn from(s: String) -> Self {
+        NixString(s.into_boxed_str())
+    }
+}
+
+impl From<Box<str>> for NixString {
+    fn from(s: Box<str>) -> Self {
+        Self(s)
+    }
+}
+
+impl From<ast::Ident> for NixString {
+    fn from(ident: ast::Ident) -> Self {
+        ident.ident_token().unwrap().text().into()
+    }
+}
+
+impl Hash for NixString {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.as_str().hash(state)
+    }
+}
+
+impl<'de> Deserialize<'de> for NixString {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct StringVisitor;
+
+        impl<'de> Visitor<'de> for StringVisitor {
+            type Value = NixString;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("a valid Nix string")
+            }
+
+            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                Ok(v.into())
+            }
+
+            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                Ok(v.into())
+            }
+        }
+
+        deserializer.deserialize_string(StringVisitor)
+    }
+}
+
+#[cfg(feature = "arbitrary")]
+mod arbitrary {
+    use super::*;
+    use proptest::prelude::{any_with, Arbitrary};
+    use proptest::strategy::{BoxedStrategy, Strategy};
+
+    impl Arbitrary for NixString {
+        type Parameters = <String as Arbitrary>::Parameters;
+
+        type Strategy = BoxedStrategy<Self>;
+
+        fn arbitrary_with(args: Self::Parameters) -> Self::Strategy {
+            any_with::<String>(args).prop_map(Self::from).boxed()
+        }
+    }
+}
+
+impl NixString {
+    pub fn as_str(&self) -> &str {
+        &self.0
+    }
+
+    /// Return a displayable representation of the string as an
+    /// identifier.
+    ///
+    /// This is used when printing out strings used as e.g. attribute
+    /// set keys, as those are only escaped in the presence of special
+    /// characters.
+    pub fn ident_str(&self) -> Cow<str> {
+        let escaped = nix_escape_string(self.as_str());
+
+        match escaped {
+            // A borrowed string is unchanged and can be returned as
+            // is.
+            Cow::Borrowed(_) => {
+                if is_valid_nix_identifier(&escaped) {
+                    escaped
+                } else {
+                    Cow::Owned(format!("\"{}\"", escaped))
+                }
+            }
+
+            // An owned string has escapes, and needs the outer quotes
+            // for display.
+            Cow::Owned(s) => Cow::Owned(format!("\"{}\"", s)),
+        }
+    }
+
+    pub fn concat(&self, other: &Self) -> Self {
+        let mut s = self.as_str().to_owned();
+        s.push_str(other.as_str());
+        NixString(s.into_boxed_str())
+    }
+
+    pub fn chars(&self) -> Chars<'_> {
+        self.0.chars()
+    }
+}
+
+fn nix_escape_char(ch: char, next: Option<&char>) -> Option<&'static str> {
+    match (ch, next) {
+        ('\\', _) => Some("\\\\"),
+        ('"', _) => Some("\\\""),
+        ('\n', _) => Some("\\n"),
+        ('\t', _) => Some("\\t"),
+        ('\r', _) => Some("\\r"),
+        ('$', Some('{')) => Some("\\$"),
+        _ => None,
+    }
+}
+
+/// Return true if this string can be used as an identifier in Nix.
+fn is_valid_nix_identifier(s: &str) -> bool {
+    // adapted from rnix-parser's tokenizer.rs
+    let mut chars = s.chars();
+    match chars.next() {
+        Some('a'..='z' | 'A'..='Z' | '_') => (),
+        _ => return false,
+    }
+    for c in chars {
+        match c {
+            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\'' => (),
+            _ => return false,
+        }
+    }
+    true
+}
+
+/// Escape a Nix string for display, as most user-visible representation
+/// are escaped strings.
+///
+/// Note that this does not add the outer pair of surrounding quotes.
+fn nix_escape_string(input: &str) -> Cow<str> {
+    let mut iter = input.chars().enumerate().peekable();
+
+    while let Some((i, c)) = iter.next() {
+        if let Some(esc) = nix_escape_char(c, iter.peek().map(|(_, c)| c)) {
+            let mut escaped = String::with_capacity(input.len());
+            escaped.push_str(&input[..i]);
+            escaped.push_str(esc);
+
+            let mut inner_iter = input[i + 1..].chars().peekable();
+            while let Some(c) = inner_iter.next() {
+                match nix_escape_char(c, inner_iter.peek()) {
+                    Some(esc) => escaped.push_str(esc),
+                    None => escaped.push(c),
+                }
+            }
+
+            return Cow::Owned(escaped);
+        }
+    }
+
+    Cow::Borrowed(input)
+}
+
+impl Display for NixString {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str("\"")?;
+        f.write_str(&nix_escape_string(self.as_str()))?;
+        f.write_str("\"")
+    }
+}
+
+impl AsRef<str> for NixString {
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl AsRef<OsStr> for NixString {
+    fn as_ref(&self) -> &OsStr {
+        self.as_str().as_ref()
+    }
+}
+
+impl AsRef<Path> for NixString {
+    fn as_ref(&self) -> &Path {
+        self.as_str().as_ref()
+    }
+}
+
+impl Deref for NixString {
+    type Target = str;
+
+    fn deref(&self) -> &Self::Target {
+        self.as_str()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    use crate::properties::{eq_laws, hash_laws, ord_laws};
+
+    eq_laws!(NixString);
+    hash_laws!(NixString);
+    ord_laws!(NixString);
+}
diff --git a/tvix/eval/src/value/thunk.rs b/tvix/eval/src/value/thunk.rs
new file mode 100644
index 000000000000..9c048d40b499
--- /dev/null
+++ b/tvix/eval/src/value/thunk.rs
@@ -0,0 +1,365 @@
+//! This module implements the runtime representation of Thunks.
+//!
+//! Thunks are a special kind of Nix value, similar to a 0-argument
+//! closure that yields some value. Thunks are used to implement the
+//! lazy evaluation behaviour of Nix:
+//!
+//! Whenever the compiler determines that an expression should be
+//! evaluated lazily, it creates a thunk instead of compiling the
+//! expression value directly. At any point in the runtime where the
+//! actual value of a thunk is required, it is "forced", meaning that
+//! the encompassing computation takes place and the thunk takes on
+//! its new value.
+//!
+//! Thunks have interior mutability to be able to memoise their
+//! computation. Once a thunk is evaluated, its internal
+//! representation becomes the result of the expression. It is legal
+//! for the runtime to replace a thunk object directly with its value
+//! object, but when forcing a thunk, the runtime *must* mutate the
+//! memoisable slot.
+
+use std::{
+    cell::{Ref, RefCell, RefMut},
+    collections::HashSet,
+    fmt::Debug,
+    rc::Rc,
+};
+
+use crate::{
+    errors::ErrorKind,
+    opcode::OpCode,
+    spans::LightSpan,
+    upvalues::Upvalues,
+    value::Closure,
+    vm::generators::{self, GenCo},
+    Value,
+};
+
+use super::{Lambda, TotalDisplay};
+use codemap::Span;
+
+/// Internal representation of a suspended native thunk.
+struct SuspendedNative(Box<dyn Fn() -> Result<Value, ErrorKind>>);
+
+impl Debug for SuspendedNative {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "SuspendedNative({:p})", self.0)
+    }
+}
+
+/// Internal representation of the different states of a thunk.
+///
+/// Upvalues must be finalised before leaving the initial state
+/// (Suspended or RecursiveClosure).  The [`value()`] function may
+/// not be called until the thunk is in the final state (Evaluated).
+#[derive(Debug)]
+enum ThunkRepr {
+    /// Thunk is closed over some values, suspended and awaiting
+    /// execution.
+    Suspended {
+        lambda: Rc<Lambda>,
+        upvalues: Rc<Upvalues>,
+        light_span: LightSpan,
+    },
+
+    /// Thunk is a suspended native computation.
+    Native(SuspendedNative),
+
+    /// Thunk currently under-evaluation; encountering a blackhole
+    /// value means that infinite recursion has occured.
+    Blackhole {
+        /// Span at which the thunk was first forced.
+        forced_at: LightSpan,
+
+        /// Span at which the thunk was originally suspended.
+        suspended_at: Option<LightSpan>,
+
+        /// Span of the first instruction of the actual code inside
+        /// the thunk.
+        content_span: Option<Span>,
+    },
+
+    /// Fully evaluated thunk.
+    Evaluated(Value),
+}
+
+impl ThunkRepr {
+    fn debug_repr(&self) -> String {
+        match self {
+            ThunkRepr::Evaluated(v) => format!("thunk(val|{})", v),
+            ThunkRepr::Blackhole { .. } => "thunk(blackhole)".to_string(),
+            ThunkRepr::Native(_) => "thunk(native)".to_string(),
+            ThunkRepr::Suspended { lambda, .. } => format!("thunk({:p})", *lambda),
+        }
+    }
+}
+
+/// A thunk is created for any value which requires non-strict
+/// evaluation due to self-reference or lazy semantics (or both).
+/// Every reference cycle involving `Value`s will contain at least
+/// one `Thunk`.
+#[derive(Clone, Debug)]
+pub struct Thunk(Rc<RefCell<ThunkRepr>>);
+
+impl Thunk {
+    pub fn new_closure(lambda: Rc<Lambda>) -> Self {
+        Thunk(Rc::new(RefCell::new(ThunkRepr::Evaluated(Value::Closure(
+            Rc::new(Closure {
+                upvalues: Rc::new(Upvalues::with_capacity(lambda.upvalue_count)),
+                lambda: lambda.clone(),
+            }),
+        )))))
+    }
+
+    pub fn new_suspended(lambda: Rc<Lambda>, light_span: LightSpan) -> Self {
+        Thunk(Rc::new(RefCell::new(ThunkRepr::Suspended {
+            upvalues: Rc::new(Upvalues::with_capacity(lambda.upvalue_count)),
+            lambda: lambda.clone(),
+            light_span,
+        })))
+    }
+
+    pub fn new_suspended_native(native: Box<dyn Fn() -> Result<Value, ErrorKind>>) -> Self {
+        Thunk(Rc::new(RefCell::new(ThunkRepr::Native(SuspendedNative(
+            native,
+        )))))
+    }
+
+    /// Helper function to create a [`Thunk`] that calls a function given as the
+    /// [`Value`] `callee` with the argument `arg` when it is forced. This is
+    /// particularly useful in builtin implementations if the result of calling
+    /// a function does not need to be forced immediately, because e.g. it is
+    /// stored in an attribute set.
+    pub fn new_suspended_call(callee: Value, arg: Value, light_span: LightSpan) -> Self {
+        let mut lambda = Lambda::default();
+        let span = light_span.span();
+
+        let arg_idx = lambda.chunk().push_constant(arg);
+        let f_idx = lambda.chunk().push_constant(callee);
+
+        // This is basically a recreation of compile_apply():
+        // We need to push the argument onto the stack and then the function.
+        // The function (not the argument) needs to be forced before calling.
+        lambda.chunk.push_op(OpCode::OpConstant(arg_idx), span);
+        lambda.chunk().push_op(OpCode::OpConstant(f_idx), span);
+        lambda.chunk.push_op(OpCode::OpForce, span);
+        lambda.chunk.push_op(OpCode::OpCall, span);
+
+        // Inform the VM that the chunk has ended
+        lambda.chunk.push_op(OpCode::OpReturn, span);
+
+        Thunk(Rc::new(RefCell::new(ThunkRepr::Suspended {
+            upvalues: Rc::new(Upvalues::with_capacity(0)),
+            lambda: Rc::new(lambda),
+            light_span,
+        })))
+    }
+
+    fn prepare_blackhole(&self, forced_at: LightSpan) -> ThunkRepr {
+        match &*self.0.borrow() {
+            ThunkRepr::Suspended {
+                light_span, lambda, ..
+            } => ThunkRepr::Blackhole {
+                forced_at,
+                suspended_at: Some(light_span.clone()),
+                content_span: Some(lambda.chunk.first_span()),
+            },
+
+            _ => ThunkRepr::Blackhole {
+                forced_at,
+                suspended_at: None,
+                content_span: None,
+            },
+        }
+    }
+
+    // TODO(amjoseph): de-asyncify this
+    pub async fn force(self, co: GenCo, span: LightSpan) -> Result<Value, ErrorKind> {
+        // If the current thunk is already fully evaluated, return its evaluated
+        // value. The VM will continue running the code that landed us here.
+        if self.is_forced() {
+            return Ok(self.value().clone());
+        }
+
+        // Begin evaluation of this thunk by marking it as a blackhole, meaning
+        // that any other forcing frame encountering this thunk before its
+        // evaluation is completed detected an evaluation cycle.
+        let inner = self.0.replace(self.prepare_blackhole(span));
+
+        match inner {
+            // If there was already a blackhole in the thunk, this is an
+            // evaluation cycle.
+            ThunkRepr::Blackhole {
+                forced_at,
+                suspended_at,
+                content_span,
+            } => Err(ErrorKind::InfiniteRecursion {
+                first_force: forced_at.span(),
+                suspended_at: suspended_at.map(|s| s.span()),
+                content_span,
+            }),
+
+            // If there is a native function stored in the thunk, evaluate it
+            // and replace this thunk's representation with the result.
+            ThunkRepr::Native(native) => {
+                let value = native.0()?;
+
+                // Force the returned value again, in case the native call
+                // returned a thunk.
+                let value = generators::request_force(&co, value).await;
+
+                self.0.replace(ThunkRepr::Evaluated(value.clone()));
+                Ok(value)
+            }
+
+            // When encountering a suspended thunk, request that the VM enters
+            // it and produces the result.
+            ThunkRepr::Suspended {
+                lambda,
+                upvalues,
+                light_span,
+            } => {
+                let value =
+                    generators::request_enter_lambda(&co, lambda, upvalues, light_span).await;
+
+                // This may have returned another thunk, so we need to request
+                // that the VM forces this value, too.
+                let value = generators::request_force(&co, value).await;
+
+                self.0.replace(ThunkRepr::Evaluated(value.clone()));
+                Ok(value)
+            }
+
+            // If an inner value is found, force it and then update. This is
+            // most likely an inner thunk, as `Thunk:is_forced` returned false.
+            ThunkRepr::Evaluated(val) => {
+                let value = generators::request_force(&co, val).await;
+                self.0.replace(ThunkRepr::Evaluated(value.clone()));
+                Ok(value)
+            }
+        }
+    }
+
+    pub fn finalise(&self, stack: &[Value]) {
+        self.upvalues_mut().resolve_deferred_upvalues(stack);
+    }
+
+    pub fn is_evaluated(&self) -> bool {
+        matches!(*self.0.borrow(), ThunkRepr::Evaluated(_))
+    }
+
+    pub fn is_suspended(&self) -> bool {
+        matches!(
+            *self.0.borrow(),
+            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_)
+        )
+    }
+
+    /// Returns true if forcing this thunk will not change it.
+    pub fn is_forced(&self) -> bool {
+        match *self.0.borrow() {
+            ThunkRepr::Evaluated(Value::Thunk(_)) => false,
+            ThunkRepr::Evaluated(_) => true,
+            _ => false,
+        }
+    }
+
+    /// Returns a reference to the inner evaluated value of a thunk.
+    /// It is an error to call this on a thunk that has not been
+    /// forced, or is not otherwise known to be fully evaluated.
+    // Note: Due to the interior mutability of thunks this is
+    // difficult to represent in the type system without impacting the
+    // API too much.
+    pub fn value(&self) -> Ref<Value> {
+        Ref::map(self.0.borrow(), |thunk| match thunk {
+            ThunkRepr::Evaluated(value) => value,
+            ThunkRepr::Blackhole { .. } => panic!("Thunk::value called on a black-holed thunk"),
+            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => {
+                panic!("Thunk::value called on a suspended thunk")
+            }
+        })
+    }
+
+    pub fn upvalues(&self) -> Ref<'_, Upvalues> {
+        Ref::map(self.0.borrow(), |thunk| match thunk {
+            ThunkRepr::Suspended { upvalues, .. } => upvalues.as_ref(),
+            ThunkRepr::Evaluated(Value::Closure(c)) => &c.upvalues,
+            _ => panic!("upvalues() on non-suspended thunk"),
+        })
+    }
+
+    pub fn upvalues_mut(&self) -> RefMut<'_, Upvalues> {
+        RefMut::map(self.0.borrow_mut(), |thunk| match thunk {
+            ThunkRepr::Suspended { upvalues, .. } => Rc::get_mut(upvalues).unwrap(),
+            ThunkRepr::Evaluated(Value::Closure(c)) => Rc::get_mut(
+                &mut Rc::get_mut(c).unwrap().upvalues,
+            )
+            .expect(
+                "upvalues_mut() was called on a thunk which already had multiple references to it",
+            ),
+            thunk => panic!("upvalues() on non-suspended thunk: {thunk:?}"),
+        })
+    }
+
+    /// Do not use this without first reading and understanding
+    /// `tvix/docs/value-pointer-equality.md`.
+    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
+        if Rc::ptr_eq(&self.0, &other.0) {
+            return true;
+        }
+        match &*self.0.borrow() {
+            ThunkRepr::Evaluated(Value::Closure(c1)) => match &*other.0.borrow() {
+                ThunkRepr::Evaluated(Value::Closure(c2)) => Rc::ptr_eq(c1, c2),
+                _ => false,
+            },
+            _ => false,
+        }
+    }
+
+    /// Helper function to format thunks in observer output.
+    pub(crate) fn debug_repr(&self) -> String {
+        self.0.borrow().debug_repr()
+    }
+}
+
+impl TotalDisplay for Thunk {
+    fn total_fmt(&self, f: &mut std::fmt::Formatter<'_>, set: &mut ThunkSet) -> std::fmt::Result {
+        if !set.insert(self) {
+            return f.write_str("<CYCLE>");
+        }
+
+        match &*self.0.borrow() {
+            ThunkRepr::Evaluated(v) => v.total_fmt(f, set),
+            ThunkRepr::Suspended { .. } | ThunkRepr::Native(_) => f.write_str("<CODE>"),
+            other => write!(f, "internal[{}]", other.debug_repr()),
+        }
+    }
+}
+
+/// A wrapper type for tracking which thunks have already been seen in a
+/// context. This is necessary for cycle detection.
+///
+/// The inner `HashSet` is not available on the outside, as it would be
+/// potentially unsafe to interact with the pointers in the set.
+#[derive(Default)]
+pub struct ThunkSet(HashSet<*const ThunkRepr>);
+
+impl ThunkSet {
+    /// Check whether the given thunk has already been seen. Will mark the thunk
+    /// as seen otherwise.
+    pub fn insert(&mut self, thunk: &Thunk) -> bool {
+        let ptr: *const ThunkRepr = thunk.0.as_ptr();
+        self.0.insert(ptr)
+    }
+}
+
+#[derive(Default, Clone)]
+pub struct SharedThunkSet(Rc<RefCell<ThunkSet>>);
+
+impl SharedThunkSet {
+    /// Check whether the given thunk has already been seen. Will mark the thunk
+    /// as seen otherwise.
+    pub fn insert(&self, thunk: &Thunk) -> bool {
+        self.0.borrow_mut().insert(thunk)
+    }
+}
diff --git a/tvix/eval/src/vm/generators.rs b/tvix/eval/src/vm/generators.rs
new file mode 100644
index 000000000000..f86683dff542
--- /dev/null
+++ b/tvix/eval/src/vm/generators.rs
@@ -0,0 +1,786 @@
+//! This module implements generator logic for the VM. Generators are functions
+//! used during evaluation which can suspend their execution during their
+//! control flow, and request that the VM do something.
+//!
+//! This is used to keep the VM's stack size constant even when evaluating
+//! deeply nested recursive data structures.
+//!
+//! We implement generators using the [`genawaiter`] crate.
+
+use core::pin::Pin;
+use genawaiter::rc::Co;
+pub use genawaiter::rc::Gen;
+use smol_str::SmolStr;
+use std::fmt::Display;
+use std::future::Future;
+
+use crate::value::{PointerEquality, SharedThunkSet};
+use crate::warnings::{EvalWarning, WarningKind};
+use crate::FileType;
+use crate::NixString;
+
+use super::*;
+
+// -- Implementation of generic generator logic.
+
+/// States that a generator can be in while being driven by the VM.
+pub(crate) enum GeneratorState {
+    /// Normal execution of the generator.
+    Running,
+
+    /// Generator is awaiting the result of a forced value.
+    AwaitingValue,
+}
+
+/// Messages that can be sent from generators *to* the VM. In most
+/// cases, the VM will suspend the generator when receiving a message
+/// and enter some other frame to process the request.
+///
+/// Responses are returned to generators via the [`GeneratorResponse`] type.
+pub enum VMRequest {
+    /// Request that the VM forces this value. This message is first sent to the
+    /// VM with the unforced value, then returned to the generator with the
+    /// forced result.
+    ForceValue(Value),
+
+    /// Request that the VM deep-forces the value.
+    DeepForceValue(Value, SharedThunkSet),
+
+    /// Request the value at the given index from the VM's with-stack, in forced
+    /// state.
+    ///
+    /// The value is returned in the `ForceValue` message.
+    WithValue(usize),
+
+    /// Request the value at the given index from the *captured* with-stack, in
+    /// forced state.
+    CapturedWithValue(usize),
+
+    /// Request that the two values be compared for Nix equality. The result is
+    /// returned in the `ForceValue` message.
+    NixEquality(Box<(Value, Value)>, PointerEquality),
+
+    /// Push the given value to the VM's stack. This is used to prepare the
+    /// stack for requesting a function call from the VM.
+    ///
+    /// The VM does not respond to this request, so the next message received is
+    /// `Empty`.
+    StackPush(Value),
+
+    /// Pop a value from the stack and return it to the generator.
+    StackPop,
+
+    /// Request that the VM coerces this value to a string.
+    StringCoerce(Value, CoercionKind),
+
+    /// Request that the VM calls the given value, with arguments already
+    /// prepared on the stack. Value must already be forced.
+    Call(Value),
+
+    /// Request a call frame entering the given lambda immediately. This can be
+    /// used to force thunks.
+    EnterLambda {
+        lambda: Rc<Lambda>,
+        upvalues: Rc<Upvalues>,
+        light_span: LightSpan,
+    },
+
+    /// Emit a runtime warning (already containing a span) through the VM.
+    EmitWarning(EvalWarning),
+
+    /// Emit a runtime warning through the VM. The span of the current generator
+    /// is used for the final warning.
+    EmitWarningKind(WarningKind),
+
+    /// Request a lookup in the VM's import cache, which tracks the
+    /// thunks yielded by previously imported files.
+    ImportCacheLookup(PathBuf),
+
+    /// Provide the VM with an imported value for a given path, which
+    /// it can populate its input cache with.
+    ImportCachePut(PathBuf, Value),
+
+    /// Request that the VM imports the given path through its I/O interface.
+    PathImport(PathBuf),
+
+    /// Request that the VM reads the given path to a string.
+    ReadToString(PathBuf),
+
+    /// Request that the VM checks whether the given path exists.
+    PathExists(PathBuf),
+
+    /// Request that the VM reads the given path.
+    ReadDir(PathBuf),
+
+    /// Request a reasonable span from the VM.
+    Span,
+
+    /// Request evaluation of `builtins.tryEval` from the VM. See
+    /// [`VM::catch_result`] for an explanation of how this works.
+    TryForce(Value),
+
+    /// Request serialisation of a value to JSON, according to the
+    /// slightly odd Nix evaluation rules.
+    ToJson(Value),
+}
+
+/// Human-readable representation of a generator message, used by observers.
+impl Display for VMRequest {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            VMRequest::ForceValue(v) => write!(f, "force_value({})", v.type_of()),
+            VMRequest::DeepForceValue(v, _) => {
+                write!(f, "deep_force_value({})", v.type_of())
+            }
+            VMRequest::WithValue(_) => write!(f, "with_value"),
+            VMRequest::CapturedWithValue(_) => write!(f, "captured_with_value"),
+            VMRequest::NixEquality(values, ptr_eq) => {
+                write!(
+                    f,
+                    "nix_eq({}, {}, PointerEquality::{:?})",
+                    values.0.type_of(),
+                    values.1.type_of(),
+                    ptr_eq
+                )
+            }
+            VMRequest::StackPush(v) => write!(f, "stack_push({})", v.type_of()),
+            VMRequest::StackPop => write!(f, "stack_pop"),
+            VMRequest::StringCoerce(v, kind) => match kind {
+                CoercionKind::Weak => write!(f, "weak_string_coerce({})", v.type_of()),
+                CoercionKind::Strong => write!(f, "strong_string_coerce({})", v.type_of()),
+            },
+            VMRequest::Call(v) => write!(f, "call({})", v),
+            VMRequest::EnterLambda { lambda, .. } => {
+                write!(f, "enter_lambda({:p})", *lambda)
+            }
+            VMRequest::EmitWarning(_) => write!(f, "emit_warning"),
+            VMRequest::EmitWarningKind(_) => write!(f, "emit_warning_kind"),
+            VMRequest::ImportCacheLookup(p) => {
+                write!(f, "import_cache_lookup({})", p.to_string_lossy())
+            }
+            VMRequest::ImportCachePut(p, _) => {
+                write!(f, "import_cache_put({})", p.to_string_lossy())
+            }
+            VMRequest::PathImport(p) => write!(f, "path_import({})", p.to_string_lossy()),
+            VMRequest::ReadToString(p) => {
+                write!(f, "read_to_string({})", p.to_string_lossy())
+            }
+            VMRequest::PathExists(p) => write!(f, "path_exists({})", p.to_string_lossy()),
+            VMRequest::ReadDir(p) => write!(f, "read_dir({})", p.to_string_lossy()),
+            VMRequest::Span => write!(f, "span"),
+            VMRequest::TryForce(v) => write!(f, "try_force({})", v.type_of()),
+            VMRequest::ToJson(v) => write!(f, "to_json({})", v.type_of()),
+        }
+    }
+}
+
+/// Responses returned to generators *from* the VM.
+pub enum VMResponse {
+    /// Empty message. Passed to the generator as the first message,
+    /// or when return values were optional.
+    Empty,
+
+    /// Value produced by the VM and returned to the generator.
+    Value(Value),
+
+    /// Path produced by the VM in response to some IO operation.
+    Path(PathBuf),
+
+    /// VM response with the contents of a directory.
+    Directory(Vec<(SmolStr, FileType)>),
+
+    /// VM response with a span to use at the current point.
+    Span(LightSpan),
+
+    /// Message returned by the VM when a catchable error is encountered during
+    /// the evaluation of `builtins.tryEval`.
+    ForceError,
+}
+
+impl Display for VMResponse {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            VMResponse::Empty => write!(f, "empty"),
+            VMResponse::Value(v) => write!(f, "value({})", v),
+            VMResponse::Path(p) => write!(f, "path({})", p.to_string_lossy()),
+            VMResponse::Directory(d) => write!(f, "dir(len = {})", d.len()),
+            VMResponse::Span(_) => write!(f, "span"),
+            VMResponse::ForceError => write!(f, "force_error"),
+        }
+    }
+}
+
+pub(crate) type Generator =
+    Gen<VMRequest, VMResponse, Pin<Box<dyn Future<Output = Result<Value, ErrorKind>>>>>;
+
+/// Helper function to provide type annotations which are otherwise difficult to
+/// infer.
+pub fn pin_generator(
+    f: impl Future<Output = Result<Value, ErrorKind>> + 'static,
+) -> Pin<Box<dyn Future<Output = Result<Value, ErrorKind>>>> {
+    Box::pin(f)
+}
+
+impl<'o> VM<'o> {
+    /// Helper function to re-enqueue the current generator while it
+    /// is awaiting a value.
+    fn reenqueue_generator(&mut self, name: &'static str, span: LightSpan, generator: Generator) {
+        self.frames.push(Frame::Generator {
+            name,
+            generator,
+            span,
+            state: GeneratorState::AwaitingValue,
+        });
+    }
+
+    /// Helper function to enqueue a new generator.
+    pub(super) fn enqueue_generator<F, G>(&mut self, name: &'static str, span: LightSpan, gen: G)
+    where
+        F: Future<Output = Result<Value, ErrorKind>> + 'static,
+        G: FnOnce(GenCo) -> F,
+    {
+        self.frames.push(Frame::Generator {
+            name,
+            span,
+            state: GeneratorState::Running,
+            generator: Gen::new(|co| pin_generator(gen(co))),
+        });
+    }
+
+    /// Run a generator frame until it yields to the outer control loop, or runs
+    /// to completion.
+    ///
+    /// The return value indicates whether the generator has completed (true),
+    /// or was suspended (false).
+    pub(crate) fn run_generator(
+        &mut self,
+        name: &'static str,
+        span: LightSpan,
+        frame_id: usize,
+        state: GeneratorState,
+        mut generator: Generator,
+        initial_message: Option<VMResponse>,
+    ) -> EvalResult<bool> {
+        // Determine what to send to the generator based on its state.
+        let mut message = match (initial_message, state) {
+            (Some(msg), _) => msg,
+            (_, GeneratorState::Running) => VMResponse::Empty,
+
+            // If control returned here, and the generator is
+            // awaiting a value, send it the top of the stack.
+            (_, GeneratorState::AwaitingValue) => VMResponse::Value(self.stack_pop()),
+        };
+
+        loop {
+            match generator.resume_with(message) {
+                // If the generator yields, it contains an instruction
+                // for what the VM should do.
+                genawaiter::GeneratorState::Yielded(request) => {
+                    self.observer.observe_generator_request(name, &request);
+
+                    match request {
+                        VMRequest::StackPush(value) => {
+                            self.stack.push(value);
+                            message = VMResponse::Empty;
+                        }
+
+                        VMRequest::StackPop => {
+                            message = VMResponse::Value(self.stack_pop());
+                        }
+
+                        // Generator has requested a force, which means that
+                        // this function prepares the frame stack and yields
+                        // back to the outer VM loop.
+                        VMRequest::ForceValue(value) => {
+                            self.reenqueue_generator(name, span.clone(), generator);
+                            self.enqueue_generator("force", span.clone(), |co| {
+                                value.force(co, span)
+                            });
+                            return Ok(false);
+                        }
+
+                        // Generator has requested a deep-force.
+                        VMRequest::DeepForceValue(value, thunk_set) => {
+                            self.reenqueue_generator(name, span.clone(), generator);
+                            self.enqueue_generator("deep_force", span, |co| {
+                                value.deep_force(co, thunk_set)
+                            });
+                            return Ok(false);
+                        }
+
+                        // Generator has requested a value from the with-stack.
+                        // Logic is similar to `ForceValue`, except with the
+                        // value being taken from that stack.
+                        VMRequest::WithValue(idx) => {
+                            self.reenqueue_generator(name, span.clone(), generator);
+
+                            let value = self.stack[self.with_stack[idx]].clone();
+                            self.enqueue_generator("force", span.clone(), |co| {
+                                value.force(co, span)
+                            });
+
+                            return Ok(false);
+                        }
+
+                        // Generator has requested a value from the *captured*
+                        // with-stack. Logic is same as above, except for the
+                        // value being from that stack.
+                        VMRequest::CapturedWithValue(idx) => {
+                            self.reenqueue_generator(name, span.clone(), generator);
+
+                            let call_frame = self.last_call_frame()
+                                .expect("Tvix bug: generator requested captured with-value, but there is no call frame");
+
+                            let value = call_frame.upvalues.with_stack().unwrap()[idx].clone();
+                            self.enqueue_generator("force", span.clone(), |co| {
+                                value.force(co, span)
+                            });
+
+                            return Ok(false);
+                        }
+
+                        VMRequest::NixEquality(values, ptr_eq) => {
+                            let values = *values;
+                            self.reenqueue_generator(name, span.clone(), generator);
+                            self.enqueue_generator("nix_eq", span, |co| {
+                                values.0.nix_eq(values.1, co, ptr_eq)
+                            });
+                            return Ok(false);
+                        }
+
+                        VMRequest::StringCoerce(val, kind) => {
+                            self.reenqueue_generator(name, span.clone(), generator);
+                            self.enqueue_generator("coerce_to_string", span, |co| {
+                                val.coerce_to_string(co, kind)
+                            });
+                            return Ok(false);
+                        }
+
+                        VMRequest::Call(callable) => {
+                            self.reenqueue_generator(name, span.clone(), generator);
+                            self.call_value(span, None, callable)?;
+                            return Ok(false);
+                        }
+
+                        VMRequest::EnterLambda {
+                            lambda,
+                            upvalues,
+                            light_span,
+                        } => {
+                            self.reenqueue_generator(name, span, generator);
+
+                            self.frames.push(Frame::CallFrame {
+                                span: light_span,
+                                call_frame: CallFrame {
+                                    lambda,
+                                    upvalues,
+                                    ip: CodeIdx(0),
+                                    stack_offset: self.stack.len(),
+                                },
+                            });
+
+                            return Ok(false);
+                        }
+
+                        VMRequest::EmitWarning(warning) => {
+                            self.push_warning(warning);
+                            message = VMResponse::Empty;
+                        }
+
+                        VMRequest::EmitWarningKind(kind) => {
+                            self.emit_warning(kind);
+                            message = VMResponse::Empty;
+                        }
+
+                        VMRequest::ImportCacheLookup(path) => {
+                            if let Some(cached) = self.import_cache.get(&path) {
+                                message = VMResponse::Value(cached.clone());
+                            } else {
+                                message = VMResponse::Empty;
+                            }
+                        }
+
+                        VMRequest::ImportCachePut(path, value) => {
+                            self.import_cache.insert(path, value);
+                            message = VMResponse::Empty;
+                        }
+
+                        VMRequest::PathImport(path) => {
+                            let imported = self
+                                .io_handle
+                                .import_path(&path)
+                                .map_err(|e| ErrorKind::IO {
+                                    path: Some(path),
+                                    error: e.into(),
+                                })
+                                .with_span(&span, self)?;
+
+                            message = VMResponse::Path(imported);
+                        }
+
+                        VMRequest::ReadToString(path) => {
+                            let content = self
+                                .io_handle
+                                .read_to_string(&path)
+                                .map_err(|e| ErrorKind::IO {
+                                    path: Some(path),
+                                    error: e.into(),
+                                })
+                                .with_span(&span, self)?;
+
+                            message = VMResponse::Value(Value::String(content.into()))
+                        }
+
+                        VMRequest::PathExists(path) => {
+                            let exists = self
+                                .io_handle
+                                .path_exists(&path)
+                                .map_err(|e| ErrorKind::IO {
+                                    path: Some(path),
+                                    error: e.into(),
+                                })
+                                .map(Value::Bool)
+                                .with_span(&span, self)?;
+
+                            message = VMResponse::Value(exists);
+                        }
+
+                        VMRequest::ReadDir(path) => {
+                            let dir = self
+                                .io_handle
+                                .read_dir(&path)
+                                .map_err(|e| ErrorKind::IO {
+                                    path: Some(path),
+                                    error: e.into(),
+                                })
+                                .with_span(&span, self)?;
+                            message = VMResponse::Directory(dir);
+                        }
+
+                        VMRequest::Span => {
+                            message = VMResponse::Span(self.reasonable_light_span());
+                        }
+
+                        VMRequest::TryForce(value) => {
+                            self.try_eval_frames.push(frame_id);
+                            self.reenqueue_generator(name, span.clone(), generator);
+
+                            debug_assert!(
+                                self.frames.len() == frame_id + 1,
+                                "generator should be reenqueued with the same frame ID"
+                            );
+
+                            self.enqueue_generator("force", span.clone(), |co| {
+                                value.force(co, span)
+                            });
+                            return Ok(false);
+                        }
+
+                        VMRequest::ToJson(value) => {
+                            self.reenqueue_generator(name, span.clone(), generator);
+                            self.enqueue_generator("to_json", span, |co| {
+                                value.to_json_generator(co)
+                            });
+                            return Ok(false);
+                        }
+                    }
+                }
+
+                // Generator has completed, and its result value should
+                // be left on the stack.
+                genawaiter::GeneratorState::Complete(result) => {
+                    let value = result.with_span(&span, self)?;
+                    self.stack.push(value);
+                    return Ok(true);
+                }
+            }
+        }
+    }
+}
+
+pub type GenCo = Co<VMRequest, VMResponse>;
+
+// -- Implementation of concrete generator use-cases.
+
+/// Request that the VM place the given value on its stack.
+pub async fn request_stack_push(co: &GenCo, val: Value) {
+    match co.yield_(VMRequest::StackPush(val)).await {
+        VMResponse::Empty => {}
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Request that the VM pop a value from the stack and return it to the
+/// generator.
+pub async fn request_stack_pop(co: &GenCo) -> Value {
+    match co.yield_(VMRequest::StackPop).await {
+        VMResponse::Value(value) => value,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Force any value and return the evaluated result from the VM.
+pub async fn request_force(co: &GenCo, val: Value) -> Value {
+    if let Value::Thunk(_) = val {
+        match co.yield_(VMRequest::ForceValue(val)).await {
+            VMResponse::Value(value) => value,
+            msg => panic!(
+                "Tvix bug: VM responded with incorrect generator message: {}",
+                msg
+            ),
+        }
+    } else {
+        val
+    }
+}
+
+/// Force a value, but inform the caller (by returning `None`) if a catchable
+/// error occured.
+pub(crate) async fn request_try_force(co: &GenCo, val: Value) -> Option<Value> {
+    if let Value::Thunk(_) = val {
+        match co.yield_(VMRequest::TryForce(val)).await {
+            VMResponse::Value(value) => Some(value),
+            VMResponse::ForceError => None,
+            msg => panic!(
+                "Tvix bug: VM responded with incorrect generator message: {}",
+                msg
+            ),
+        }
+    } else {
+        Some(val)
+    }
+}
+
+/// Call the given value as a callable. The argument(s) must already be prepared
+/// on the stack.
+pub async fn request_call(co: &GenCo, val: Value) -> Value {
+    let val = request_force(co, val).await;
+    match co.yield_(VMRequest::Call(val)).await {
+        VMResponse::Value(value) => value,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Helper function to call the given value with the provided list of arguments.
+/// This uses the StackPush and Call messages under the hood.
+pub async fn request_call_with<I>(co: &GenCo, mut callable: Value, args: I) -> Value
+where
+    I: IntoIterator<Item = Value>,
+    I::IntoIter: DoubleEndedIterator,
+{
+    let mut num_args = 0_usize;
+    for arg in args.into_iter().rev() {
+        num_args += 1;
+        request_stack_push(co, arg).await;
+    }
+
+    debug_assert!(num_args > 0, "call_with called with an empty list of args");
+
+    while num_args > 0 {
+        callable = request_call(co, callable).await;
+        num_args -= 1;
+    }
+
+    callable
+}
+
+pub async fn request_string_coerce(co: &GenCo, val: Value, kind: CoercionKind) -> NixString {
+    match val {
+        Value::String(s) => s,
+        _ => match co.yield_(VMRequest::StringCoerce(val, kind)).await {
+            VMResponse::Value(value) => value
+                .to_str()
+                .expect("coerce_to_string always returns a string"),
+            msg => panic!(
+                "Tvix bug: VM responded with incorrect generator message: {}",
+                msg
+            ),
+        },
+    }
+}
+
+/// Deep-force any value and return the evaluated result from the VM.
+pub async fn request_deep_force(co: &GenCo, val: Value, thunk_set: SharedThunkSet) -> Value {
+    match co.yield_(VMRequest::DeepForceValue(val, thunk_set)).await {
+        VMResponse::Value(value) => value,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Ask the VM to compare two values for equality.
+pub(crate) async fn check_equality(
+    co: &GenCo,
+    a: Value,
+    b: Value,
+    ptr_eq: PointerEquality,
+) -> Result<bool, ErrorKind> {
+    match co
+        .yield_(VMRequest::NixEquality(Box::new((a, b)), ptr_eq))
+        .await
+    {
+        VMResponse::Value(value) => value.as_bool(),
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Emit a fully constructed runtime warning.
+pub(crate) async fn emit_warning(co: &GenCo, warning: EvalWarning) {
+    match co.yield_(VMRequest::EmitWarning(warning)).await {
+        VMResponse::Empty => {}
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Emit a runtime warning with the span of the current generator.
+pub(crate) async fn emit_warning_kind(co: &GenCo, kind: WarningKind) {
+    match co.yield_(VMRequest::EmitWarningKind(kind)).await {
+        VMResponse::Empty => {}
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Request that the VM enter the given lambda.
+pub(crate) async fn request_enter_lambda(
+    co: &GenCo,
+    lambda: Rc<Lambda>,
+    upvalues: Rc<Upvalues>,
+    light_span: LightSpan,
+) -> Value {
+    let msg = VMRequest::EnterLambda {
+        lambda,
+        upvalues,
+        light_span,
+    };
+
+    match co.yield_(msg).await {
+        VMResponse::Value(value) => value,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Request a lookup in the VM's import cache.
+pub(crate) async fn request_import_cache_lookup(co: &GenCo, path: PathBuf) -> Option<Value> {
+    match co.yield_(VMRequest::ImportCacheLookup(path)).await {
+        VMResponse::Value(value) => Some(value),
+        VMResponse::Empty => None,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Request that the VM populate its input cache for the given path.
+pub(crate) async fn request_import_cache_put(co: &GenCo, path: PathBuf, value: Value) {
+    match co.yield_(VMRequest::ImportCachePut(path, value)).await {
+        VMResponse::Empty => {}
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Request that the VM import the given path.
+pub(crate) async fn request_path_import(co: &GenCo, path: PathBuf) -> PathBuf {
+    match co.yield_(VMRequest::PathImport(path)).await {
+        VMResponse::Path(path) => path,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+pub(crate) async fn request_read_to_string(co: &GenCo, path: PathBuf) -> Value {
+    match co.yield_(VMRequest::ReadToString(path)).await {
+        VMResponse::Value(value) => value,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+pub(crate) async fn request_path_exists(co: &GenCo, path: PathBuf) -> Value {
+    match co.yield_(VMRequest::PathExists(path)).await {
+        VMResponse::Value(value) => value,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+pub(crate) async fn request_read_dir(co: &GenCo, path: PathBuf) -> Vec<(SmolStr, FileType)> {
+    match co.yield_(VMRequest::ReadDir(path)).await {
+        VMResponse::Directory(dir) => dir,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+pub(crate) async fn request_span(co: &GenCo) -> LightSpan {
+    match co.yield_(VMRequest::Span).await {
+        VMResponse::Span(span) => span,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+pub(crate) async fn request_to_json(co: &GenCo, value: Value) -> serde_json::Value {
+    match co.yield_(VMRequest::ToJson(value)).await {
+        VMResponse::Value(Value::Json(json)) => json,
+        msg => panic!(
+            "Tvix bug: VM responded with incorrect generator message: {}",
+            msg
+        ),
+    }
+}
+
+/// Call the given value as if it was an attribute set containing a functor. The
+/// arguments must already be prepared on the stack when a generator frame from
+/// this function is invoked.
+///
+pub(crate) async fn call_functor(co: GenCo, value: Value) -> Result<Value, ErrorKind> {
+    let attrs = value.to_attrs()?;
+
+    match attrs.select("__functor") {
+        None => Err(ErrorKind::NotCallable("set without `__functor_` attribute")),
+        Some(functor) => {
+            // The functor receives the set itself as its first argument and
+            // needs to be called with it.
+            let functor = request_force(&co, functor.clone()).await;
+            let primed = request_call_with(&co, functor, [value]).await;
+            Ok(request_call(&co, primed).await)
+        }
+    }
+}
diff --git a/tvix/eval/src/vm/macros.rs b/tvix/eval/src/vm/macros.rs
new file mode 100644
index 000000000000..969b50807766
--- /dev/null
+++ b/tvix/eval/src/vm/macros.rs
@@ -0,0 +1,70 @@
+/// This module provides macros which are used in the implementation
+/// of the VM for the implementation of repetitive operations.
+
+/// This macro simplifies the implementation of arithmetic operations,
+/// correctly handling the behaviour on different pairings of number
+/// types.
+#[macro_export]
+macro_rules! arithmetic_op {
+    ( $self:ident, $op:tt ) => {{ // TODO: remove
+        let b = $self.pop();
+        let a = $self.pop();
+        let result = fallible!($self, arithmetic_op!(&a, &b, $op));
+        $self.push(result);
+    }};
+
+    ( $a:expr, $b:expr, $op:tt ) => {{
+        match ($a, $b) {
+            (Value::Integer(i1), Value::Integer(i2)) => Ok(Value::Integer(i1 $op i2)),
+            (Value::Float(f1), Value::Float(f2)) => Ok(Value::Float(f1 $op f2)),
+            (Value::Integer(i1), Value::Float(f2)) => Ok(Value::Float(*i1 as f64 $op f2)),
+            (Value::Float(f1), Value::Integer(i2)) => Ok(Value::Float(f1 $op *i2 as f64)),
+
+            (v1, v2) => Err(ErrorKind::TypeError {
+                expected: "number (either int or float)",
+                actual: if v1.is_number() {
+                    v2.type_of()
+                } else {
+                    v1.type_of()
+                },
+            }),
+        }
+    }};
+}
+
+/// This macro simplifies the implementation of comparison operations.
+#[macro_export]
+macro_rules! cmp_op {
+    ( $vm:ident, $frame:ident, $span:ident, $op:tt ) => {{
+        let b = $vm.stack_pop();
+        let a = $vm.stack_pop();
+
+        async fn compare(a: Value, b: Value, co: GenCo) -> Result<Value, ErrorKind> {
+            let a = generators::request_force(&co, a).await;
+            let b = generators::request_force(&co, b).await;
+            let ordering = a.nix_cmp_ordering(b, co).await?;
+            Ok(Value::Bool(cmp_op!(@order $op ordering)))
+        }
+
+        let gen_span = $frame.current_light_span();
+        $vm.push_call_frame($span, $frame);
+        $vm.enqueue_generator("compare", gen_span, |co| compare(a, b, co));
+        return Ok(false);
+    }};
+
+    (@order < $ordering:expr) => {
+        $ordering == Some(Ordering::Less)
+    };
+
+    (@order > $ordering:expr) => {
+        $ordering == Some(Ordering::Greater)
+    };
+
+    (@order <= $ordering:expr) => {
+        !matches!($ordering, None | Some(Ordering::Greater))
+    };
+
+    (@order >= $ordering:expr) => {
+        !matches!($ordering, None | Some(Ordering::Less))
+    };
+}
diff --git a/tvix/eval/src/vm/mod.rs b/tvix/eval/src/vm/mod.rs
new file mode 100644
index 000000000000..4af23a72d73b
--- /dev/null
+++ b/tvix/eval/src/vm/mod.rs
@@ -0,0 +1,1235 @@
+//! This module implements the abstract/virtual machine that runs Tvix
+//! bytecode.
+//!
+//! The operation of the VM is facilitated by the [`Frame`] type,
+//! which controls the current execution state of the VM and is
+//! processed within the VM's operating loop.
+//!
+//! A [`VM`] is used by instantiating it with an initial [`Frame`],
+//! then triggering its execution and waiting for the VM to return or
+//! yield an error.
+
+pub mod generators;
+mod macros;
+
+use codemap::Span;
+use serde_json::json;
+use std::{cmp::Ordering, collections::HashMap, ops::DerefMut, path::PathBuf, rc::Rc};
+
+use crate::{
+    arithmetic_op,
+    chunk::Chunk,
+    cmp_op,
+    compiler::GlobalsMap,
+    errors::{Error, ErrorKind, EvalResult},
+    io::EvalIO,
+    nix_search_path::NixSearchPath,
+    observer::RuntimeObserver,
+    opcode::{CodeIdx, Count, JumpOffset, OpCode, StackIdx, UpvalueIdx},
+    spans::LightSpan,
+    upvalues::Upvalues,
+    value::{
+        Builtin, BuiltinResult, Closure, CoercionKind, Lambda, NixAttrs, NixList, PointerEquality,
+        SharedThunkSet, Thunk, Value,
+    },
+    vm::generators::GenCo,
+    warnings::{EvalWarning, WarningKind},
+};
+
+use generators::{call_functor, Generator, GeneratorState};
+
+use self::generators::{VMRequest, VMResponse};
+
+/// Internal helper trait for taking a span from a variety of types, to make use
+/// of `WithSpan` (defined below) more ergonomic at call sites.
+trait GetSpan {
+    fn get_span(self) -> Span;
+}
+
+impl<'o> GetSpan for &VM<'o> {
+    fn get_span(self) -> Span {
+        self.reasonable_span.span()
+    }
+}
+
+impl GetSpan for &CallFrame {
+    fn get_span(self) -> Span {
+        self.current_span()
+    }
+}
+
+impl GetSpan for &LightSpan {
+    fn get_span(self) -> Span {
+        self.span()
+    }
+}
+
+impl GetSpan for Span {
+    fn get_span(self) -> Span {
+        self
+    }
+}
+
+/// Internal helper trait for ergonomically converting from a `Result<T,
+/// ErrorKind>` to a `Result<T, Error>` using the current span of a call frame,
+/// and chaining the VM's frame stack around it for printing a cause chain.
+trait WithSpan<T, S: GetSpan> {
+    fn with_span(self, top_span: S, vm: &VM) -> Result<T, Error>;
+}
+
+impl<T, S: GetSpan> WithSpan<T, S> for Result<T, ErrorKind> {
+    fn with_span(self, top_span: S, vm: &VM) -> Result<T, Error> {
+        match self {
+            Ok(something) => Ok(something),
+            Err(kind) => {
+                let mut error = Error::new(kind, top_span.get_span());
+
+                // Short-circuit the wrapping if we're dealing with tryEval, in
+                // which case the error is hidden and does not need to be
+                // exhaustive.
+                if !vm.try_eval_frames.is_empty() && error.kind.is_catchable() {
+                    return Err(error);
+                }
+
+                // Wrap the top-level error in chaining errors for each element
+                // of the frame stack.
+                for frame in vm.frames.iter().rev() {
+                    match frame {
+                        Frame::CallFrame { span, .. } => {
+                            error =
+                                Error::new(ErrorKind::BytecodeError(Box::new(error)), span.span());
+                        }
+                        Frame::Generator { name, span, .. } => {
+                            error = Error::new(
+                                ErrorKind::NativeError {
+                                    err: Box::new(error),
+                                    gen_type: name,
+                                },
+                                span.span(),
+                            );
+                        }
+                    }
+                }
+
+                Err(error)
+            }
+        }
+    }
+}
+
+struct CallFrame {
+    /// The lambda currently being executed.
+    lambda: Rc<Lambda>,
+
+    /// Optional captured upvalues of this frame (if a thunk or
+    /// closure if being evaluated).
+    upvalues: Rc<Upvalues>,
+
+    /// Instruction pointer to the instruction currently being
+    /// executed.
+    ip: CodeIdx,
+
+    /// Stack offset, i.e. the frames "view" into the VM's full stack.
+    stack_offset: usize,
+}
+
+impl CallFrame {
+    /// Retrieve an upvalue from this frame at the given index.
+    fn upvalue(&self, idx: UpvalueIdx) -> &Value {
+        &self.upvalues[idx]
+    }
+
+    /// Borrow the chunk of this frame's lambda.
+    fn chunk(&self) -> &Chunk {
+        &self.lambda.chunk
+    }
+
+    /// Increment this frame's instruction pointer and return the operation that
+    /// the pointer moved past.
+    fn inc_ip(&mut self) -> OpCode {
+        let op = self.chunk()[self.ip];
+        self.ip += 1;
+        op
+    }
+
+    /// Construct an error result from the given ErrorKind and the source span
+    /// of the current instruction.
+    pub fn error<T>(&self, vm: &VM, kind: ErrorKind) -> Result<T, Error> {
+        Err(kind).with_span(self, vm)
+    }
+
+    /// Returns the current span. This is potentially expensive and should only
+    /// be used when actually constructing an error or warning.
+    pub fn current_span(&self) -> Span {
+        self.chunk().get_span(self.ip - 1)
+    }
+
+    /// Returns the information needed to calculate the current span,
+    /// but without performing that calculation.
+    // TODO: why pub?
+    pub(crate) fn current_light_span(&self) -> LightSpan {
+        LightSpan::new_delayed(self.lambda.clone(), self.ip - 1)
+    }
+}
+
+/// A frame represents an execution state of the VM. The VM has a stack of
+/// frames representing the nesting of execution inside of the VM, and operates
+/// on the frame at the top.
+///
+/// When a frame has been fully executed, it is removed from the VM's frame
+/// stack and expected to leave a result [`Value`] on the top of the stack.
+enum Frame {
+    /// CallFrame represents the execution of Tvix bytecode within a thunk,
+    /// function or closure.
+    CallFrame {
+        /// The call frame itself, separated out into another type to pass it
+        /// around easily.
+        call_frame: CallFrame,
+
+        /// Span from which the call frame was launched.
+        span: LightSpan,
+    },
+
+    /// Generator represents a frame that can yield further
+    /// instructions to the VM while its execution is being driven.
+    ///
+    /// A generator is essentially an asynchronous function that can
+    /// be suspended while waiting for the VM to do something (e.g.
+    /// thunk forcing), and resume at the same point.
+    Generator {
+        /// human-readable description of the generator,
+        name: &'static str,
+
+        /// Span from which the generator was launched.
+        span: LightSpan,
+
+        state: GeneratorState,
+
+        /// Generator itself, which can be resumed with `.resume()`.
+        generator: Generator,
+    },
+}
+
+impl Frame {
+    pub fn span(&self) -> LightSpan {
+        match self {
+            Frame::CallFrame { span, .. } | Frame::Generator { span, .. } => span.clone(),
+        }
+    }
+}
+
+struct VM<'o> {
+    /// VM's frame stack, representing the execution contexts the VM is working
+    /// through. Elements are usually pushed when functions are called, or
+    /// thunks are being forced.
+    frames: Vec<Frame>,
+
+    /// The VM's top-level value stack. Within this stack, each code-executing
+    /// frame holds a "view" of the stack representing the slice of the
+    /// top-level stack that is relevant to its operation. This is done to avoid
+    /// allocating a new `Vec` for each frame's stack.
+    pub(crate) stack: Vec<Value>,
+
+    /// Stack indices (absolute indexes into `stack`) of attribute
+    /// sets from which variables should be dynamically resolved
+    /// (`with`).
+    with_stack: Vec<usize>,
+
+    /// Runtime warnings collected during evaluation.
+    warnings: Vec<EvalWarning>,
+
+    /// Import cache, mapping absolute file paths to the value that
+    /// they compile to. Note that this reuses thunks, too!
+    // TODO: should probably be based on a file hash
+    pub import_cache: Box<HashMap<PathBuf, Value>>,
+
+    /// Parsed Nix search path, which is used to resolve `<...>`
+    /// references.
+    nix_search_path: NixSearchPath,
+
+    /// Implementation of I/O operations used for impure builtins and
+    /// features like `import`.
+    io_handle: Box<dyn EvalIO>,
+
+    /// Runtime observer which can print traces of runtime operations.
+    observer: &'o mut dyn RuntimeObserver,
+
+    /// Strong reference to the globals, guaranteeing that they are
+    /// kept alive for the duration of evaluation.
+    ///
+    /// This is important because recursive builtins (specifically
+    /// `import`) hold a weak reference to the builtins, while the
+    /// original strong reference is held by the compiler which does
+    /// not exist anymore at runtime.
+    #[allow(dead_code)]
+    globals: Rc<GlobalsMap>,
+
+    /// A reasonably applicable span that can be used for errors in each
+    /// execution situation.
+    ///
+    /// The VM should update this whenever control flow changes take place (i.e.
+    /// entering or exiting a frame to yield control somewhere).
+    reasonable_span: LightSpan,
+
+    /// This field is responsible for handling `builtins.tryEval`. When that
+    /// builtin is encountered, it sends a special message to the VM which
+    /// pushes the frame index that requested to be informed of catchable
+    /// errors in this field.
+    ///
+    /// The frame stack is then laid out like this:
+    ///
+    /// ```notrust
+    /// ┌──┬──────────────────────────┐
+    /// │ 0│ `Result`-producing frame │
+    /// ├──┼──────────────────────────┤
+    /// │-1│ `builtins.tryEval` frame │
+    /// ├──┼──────────────────────────┤
+    /// │..│ ... other frames ...     │
+    /// └──┴──────────────────────────┘
+    /// ```
+    ///
+    /// Control is yielded to the outer VM loop, which evaluates the next frame
+    /// and returns the result itself to the `builtins.tryEval` frame.
+    try_eval_frames: Vec<usize>,
+}
+
+impl<'o> VM<'o> {
+    pub fn new(
+        nix_search_path: NixSearchPath,
+        io_handle: Box<dyn EvalIO>,
+        observer: &'o mut dyn RuntimeObserver,
+        globals: Rc<GlobalsMap>,
+        reasonable_span: LightSpan,
+    ) -> Self {
+        Self {
+            nix_search_path,
+            io_handle,
+            observer,
+            globals,
+            reasonable_span,
+            frames: vec![],
+            stack: vec![],
+            with_stack: vec![],
+            warnings: vec![],
+            import_cache: Default::default(),
+            try_eval_frames: vec![],
+        }
+    }
+
+    /// Push a call frame onto the frame stack.
+    fn push_call_frame(&mut self, span: LightSpan, call_frame: CallFrame) {
+        self.frames.push(Frame::CallFrame { span, call_frame })
+    }
+
+    /// Run the VM's primary (outer) execution loop, continuing execution based
+    /// on the current frame at the top of the frame stack.
+    fn execute(mut self) -> EvalResult<RuntimeResult> {
+        let mut catchable_error_occurred = false;
+
+        while let Some(frame) = self.frames.pop() {
+            self.reasonable_span = frame.span();
+            let frame_id = self.frames.len();
+
+            match frame {
+                Frame::CallFrame { call_frame, span } => {
+                    self.observer
+                        .observe_enter_call_frame(0, &call_frame.lambda, frame_id);
+
+                    match self.execute_bytecode(span, call_frame) {
+                        Ok(true) => self.observer.observe_exit_call_frame(frame_id, &self.stack),
+                        Ok(false) => self
+                            .observer
+                            .observe_suspend_call_frame(frame_id, &self.stack),
+
+                        Err(err) => {
+                            if let Some(catching_frame_idx) = self.try_eval_frames.pop() {
+                                if err.kind.is_catchable() {
+                                    self.observer.observe_exit_call_frame(frame_id, &self.stack);
+                                    catchable_error_occurred = true;
+
+                                    // truncate the frame stack back to the
+                                    // frame that can catch this error
+                                    self.frames.truncate(/* len = */ catching_frame_idx + 1);
+                                    continue;
+                                }
+                            }
+
+                            return Err(err);
+                        }
+                    };
+                }
+
+                // Handle generator frames, which can request thunk forcing
+                // during their execution.
+                Frame::Generator {
+                    name,
+                    span,
+                    state,
+                    generator,
+                } => {
+                    self.observer
+                        .observe_enter_generator(frame_id, name, &self.stack);
+
+                    let initial_msg = if catchable_error_occurred {
+                        catchable_error_occurred = false;
+                        Some(VMResponse::ForceError)
+                    } else {
+                        None
+                    };
+
+                    match self.run_generator(name, span, frame_id, state, generator, initial_msg) {
+                        Ok(true) => {
+                            self.observer
+                                .observe_exit_generator(frame_id, name, &self.stack)
+                        }
+                        Ok(false) => {
+                            self.observer
+                                .observe_suspend_generator(frame_id, name, &self.stack)
+                        }
+
+                        Err(err) => {
+                            if let Some(catching_frame_idx) = self.try_eval_frames.pop() {
+                                if err.kind.is_catchable() {
+                                    self.observer.observe_exit_generator(
+                                        frame_id,
+                                        name,
+                                        &self.stack,
+                                    );
+                                    catchable_error_occurred = true;
+
+                                    // truncate the frame stack back to the
+                                    // frame that can catch this error
+                                    self.frames.truncate(/* len = */ catching_frame_idx + 1);
+                                    continue;
+                                }
+                            }
+
+                            return Err(err);
+                        }
+                    };
+                }
+            }
+        }
+
+        // Once no more frames are present, return the stack's top value as the
+        // result.
+        Ok(RuntimeResult {
+            value: self
+                .stack
+                .pop()
+                .expect("tvix bug: runtime stack empty after execution"),
+
+            warnings: self.warnings,
+        })
+    }
+
+    /// Run the VM's inner execution loop, processing Tvix bytecode from a
+    /// chunk. This function returns if:
+    ///
+    /// 1. The code has run to the end, and has left a value on the top of the
+    ///    stack. In this case, the frame is not returned to the frame stack.
+    ///
+    /// 2. The code encounters a generator, in which case the frame in its
+    /// current state is pushed back on the stack, and the generator is left on
+    /// top of it for the outer loop to execute.
+    ///
+    /// 3. An error is encountered.
+    ///
+    /// This function *must* ensure that it leaves the frame stack in the
+    /// correct order, especially when re-enqueuing a frame to execute.
+    ///
+    /// The return value indicates whether the bytecode has been executed to
+    /// completion, or whether it has been suspended in favour of a generator.
+    fn execute_bytecode(&mut self, span: LightSpan, mut frame: CallFrame) -> EvalResult<bool> {
+        loop {
+            let op = frame.inc_ip();
+            self.observer.observe_execute_op(frame.ip, &op, &self.stack);
+
+            match op {
+                OpCode::OpThunkSuspended(idx) | OpCode::OpThunkClosure(idx) => {
+                    let blueprint = match &frame.chunk()[idx] {
+                        Value::Blueprint(lambda) => lambda.clone(),
+                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
+                    };
+
+                    let upvalue_count = blueprint.upvalue_count;
+                    let thunk = if matches!(op, OpCode::OpThunkClosure(_)) {
+                        debug_assert!(
+                            upvalue_count > 0,
+                            "OpThunkClosure should not be called for plain lambdas"
+                        );
+                        Thunk::new_closure(blueprint)
+                    } else {
+                        Thunk::new_suspended(blueprint, frame.current_light_span())
+                    };
+                    let upvalues = thunk.upvalues_mut();
+                    self.stack.push(Value::Thunk(thunk.clone()));
+
+                    // From this point on we internally mutate the
+                    // upvalues. The closure (if `is_closure`) is
+                    // already in its stack slot, which means that it
+                    // can capture itself as an upvalue for
+                    // self-recursion.
+                    self.populate_upvalues(&mut frame, upvalue_count, upvalues)?;
+                }
+
+                OpCode::OpForce => {
+                    if let Some(Value::Thunk(_)) = self.stack.last() {
+                        let thunk = match self.stack_pop() {
+                            Value::Thunk(t) => t,
+                            _ => unreachable!(),
+                        };
+
+                        let gen_span = frame.current_light_span();
+
+                        self.push_call_frame(span, frame);
+                        self.enqueue_generator("force", gen_span.clone(), |co| {
+                            thunk.force(co, gen_span)
+                        });
+
+                        return Ok(false);
+                    }
+                }
+
+                OpCode::OpGetUpvalue(upv_idx) => {
+                    let value = frame.upvalue(upv_idx).clone();
+                    self.stack.push(value);
+                }
+
+                // Discard the current frame.
+                OpCode::OpReturn => {
+                    return Ok(true);
+                }
+
+                OpCode::OpConstant(idx) => {
+                    let c = frame.chunk()[idx].clone();
+                    self.stack.push(c);
+                }
+
+                OpCode::OpCall => {
+                    let callable = self.stack_pop();
+                    self.call_value(frame.current_light_span(), Some((span, frame)), callable)?;
+
+                    // exit this loop and let the outer loop enter the new call
+                    return Ok(true);
+                }
+
+                // Remove the given number of elements from the stack,
+                // but retain the top value.
+                OpCode::OpCloseScope(Count(count)) => {
+                    // Immediately move the top value into the right
+                    // position.
+                    let target_idx = self.stack.len() - 1 - count;
+                    self.stack[target_idx] = self.stack_pop();
+
+                    // Then drop the remaining values.
+                    for _ in 0..(count - 1) {
+                        self.stack.pop();
+                    }
+                }
+
+                OpCode::OpClosure(idx) => {
+                    let blueprint = match &frame.chunk()[idx] {
+                        Value::Blueprint(lambda) => lambda.clone(),
+                        _ => panic!("compiler bug: non-blueprint in blueprint slot"),
+                    };
+
+                    let upvalue_count = blueprint.upvalue_count;
+                    debug_assert!(
+                        upvalue_count > 0,
+                        "OpClosure should not be called for plain lambdas"
+                    );
+
+                    let mut upvalues = Upvalues::with_capacity(blueprint.upvalue_count);
+                    self.populate_upvalues(&mut frame, upvalue_count, &mut upvalues)?;
+                    self.stack
+                        .push(Value::Closure(Rc::new(Closure::new_with_upvalues(
+                            Rc::new(upvalues),
+                            blueprint,
+                        ))));
+                }
+
+                OpCode::OpAttrsSelect => {
+                    let key = self.stack_pop().to_str().with_span(&frame, self)?;
+                    let attrs = self.stack_pop().to_attrs().with_span(&frame, self)?;
+
+                    match attrs.select(key.as_str()) {
+                        Some(value) => self.stack.push(value.clone()),
+
+                        None => {
+                            return frame.error(
+                                self,
+                                ErrorKind::AttributeNotFound {
+                                    name: key.as_str().to_string(),
+                                },
+                            );
+                        }
+                    }
+                }
+
+                OpCode::OpJumpIfFalse(JumpOffset(offset)) => {
+                    debug_assert!(offset != 0);
+                    if !self.stack_peek(0).as_bool().with_span(&frame, self)? {
+                        frame.ip += offset;
+                    }
+                }
+
+                OpCode::OpPop => {
+                    self.stack.pop();
+                }
+
+                OpCode::OpAttrsTrySelect => {
+                    let key = self.stack_pop().to_str().with_span(&frame, self)?;
+                    let value = match self.stack_pop() {
+                        Value::Attrs(attrs) => match attrs.select(key.as_str()) {
+                            Some(value) => value.clone(),
+                            None => Value::AttrNotFound,
+                        },
+
+                        _ => Value::AttrNotFound,
+                    };
+
+                    self.stack.push(value);
+                }
+
+                OpCode::OpGetLocal(StackIdx(local_idx)) => {
+                    let idx = frame.stack_offset + local_idx;
+                    self.stack.push(self.stack[idx].clone());
+                }
+
+                OpCode::OpJumpIfNotFound(JumpOffset(offset)) => {
+                    debug_assert!(offset != 0);
+                    if matches!(self.stack_peek(0), Value::AttrNotFound) {
+                        self.stack_pop();
+                        frame.ip += offset;
+                    }
+                }
+
+                OpCode::OpJump(JumpOffset(offset)) => {
+                    debug_assert!(offset != 0);
+                    frame.ip += offset;
+                }
+
+                OpCode::OpEqual => {
+                    let b = self.stack_pop();
+                    let a = self.stack_pop();
+                    let gen_span = frame.current_light_span();
+                    self.push_call_frame(span, frame);
+                    self.enqueue_generator("nix_eq", gen_span, |co| {
+                        a.nix_eq(b, co, PointerEquality::ForbidAll)
+                    });
+                    return Ok(false);
+                }
+
+                // These assertion operations error out if the stack
+                // top is not of the expected type. This is necessary
+                // to implement some specific behaviours of Nix
+                // exactly.
+                OpCode::OpAssertBool => {
+                    let val = self.stack_peek(0);
+                    if !val.is_bool() {
+                        return frame.error(
+                            self,
+                            ErrorKind::TypeError {
+                                expected: "bool",
+                                actual: val.type_of(),
+                            },
+                        );
+                    }
+                }
+
+                OpCode::OpAttrs(Count(count)) => self.run_attrset(&frame, count)?,
+
+                OpCode::OpAttrsUpdate => {
+                    let rhs = self.stack_pop().to_attrs().with_span(&frame, self)?;
+                    let lhs = self.stack_pop().to_attrs().with_span(&frame, self)?;
+
+                    self.stack.push(Value::attrs(lhs.update(*rhs)))
+                }
+
+                OpCode::OpInvert => {
+                    let v = self.stack_pop().as_bool().with_span(&frame, self)?;
+                    self.stack.push(Value::Bool(!v));
+                }
+
+                OpCode::OpList(Count(count)) => {
+                    let list =
+                        NixList::construct(count, self.stack.split_off(self.stack.len() - count));
+
+                    self.stack.push(Value::List(list));
+                }
+
+                OpCode::OpJumpIfTrue(JumpOffset(offset)) => {
+                    debug_assert!(offset != 0);
+                    if self.stack_peek(0).as_bool().with_span(&frame, self)? {
+                        frame.ip += offset;
+                    }
+                }
+
+                OpCode::OpHasAttr => {
+                    let key = self.stack_pop().to_str().with_span(&frame, self)?;
+                    let result = match self.stack_pop() {
+                        Value::Attrs(attrs) => attrs.contains(key.as_str()),
+
+                        // Nix allows use of `?` on non-set types, but
+                        // always returns false in those cases.
+                        _ => false,
+                    };
+
+                    self.stack.push(Value::Bool(result));
+                }
+
+                OpCode::OpConcat => {
+                    let rhs = self
+                        .stack_pop()
+                        .to_list()
+                        .with_span(&frame, self)?
+                        .into_inner();
+                    let lhs = self
+                        .stack_pop()
+                        .to_list()
+                        .with_span(&frame, self)?
+                        .into_inner();
+                    self.stack.push(Value::List(NixList::from(lhs + rhs)))
+                }
+
+                OpCode::OpResolveWith => {
+                    let ident = self.stack_pop().to_str().with_span(&frame, self)?;
+
+                    // Re-enqueue this frame.
+                    let op_span = frame.current_light_span();
+                    self.push_call_frame(span, frame);
+
+                    // Construct a generator frame doing the lookup in constant
+                    // stack space.
+                    let with_stack_len = self.with_stack.len();
+                    let closed_with_stack_len = self
+                        .last_call_frame()
+                        .map(|frame| frame.upvalues.with_stack_len())
+                        .unwrap_or(0);
+
+                    self.enqueue_generator("resolve_with", op_span, |co| {
+                        resolve_with(
+                            co,
+                            ident.as_str().to_owned(),
+                            with_stack_len,
+                            closed_with_stack_len,
+                        )
+                    });
+
+                    return Ok(false);
+                }
+
+                OpCode::OpFinalise(StackIdx(idx)) => {
+                    match &self.stack[frame.stack_offset + idx] {
+                        Value::Closure(_) => panic!("attempted to finalise a closure"),
+                        Value::Thunk(thunk) => thunk.finalise(&self.stack[frame.stack_offset..]),
+
+                        // In functions with "formals" attributes, it is
+                        // possible for `OpFinalise` to be called on a
+                        // non-capturing value, in which case it is a no-op.
+                        //
+                        // TODO: detect this in some phase and skip the finalise; fail here
+                        _ => { /* TODO: panic here again to catch bugs */ }
+                    }
+                }
+
+                OpCode::OpCoerceToString => {
+                    let value = self.stack_pop();
+                    let gen_span = frame.current_light_span();
+                    self.push_call_frame(span, frame);
+
+                    self.enqueue_generator("coerce_to_string", gen_span, |co| {
+                        value.coerce_to_string(co, CoercionKind::Weak)
+                    });
+
+                    return Ok(false);
+                }
+
+                OpCode::OpInterpolate(Count(count)) => self.run_interpolate(&frame, count)?,
+
+                OpCode::OpValidateClosedFormals => {
+                    let formals = frame.lambda.formals.as_ref().expect(
+                        "OpValidateClosedFormals called within the frame of a lambda without formals",
+                    );
+
+                    let args = self.stack_peek(0).to_attrs().with_span(&frame, self)?;
+                    for arg in args.keys() {
+                        if !formals.contains(arg) {
+                            return frame.error(
+                                self,
+                                ErrorKind::UnexpectedArgument {
+                                    arg: arg.clone(),
+                                    formals_span: formals.span,
+                                },
+                            );
+                        }
+                    }
+                }
+
+                OpCode::OpAdd => {
+                    let b = self.stack_pop();
+                    let a = self.stack_pop();
+
+                    let gen_span = frame.current_light_span();
+                    self.push_call_frame(span, frame);
+
+                    // OpAdd can add not just numbers, but also string-like
+                    // things, which requires more VM logic. This operation is
+                    // evaluated in a generator frame.
+                    self.enqueue_generator("add_values", gen_span, |co| add_values(co, a, b));
+                    return Ok(false);
+                }
+
+                OpCode::OpSub => {
+                    let b = self.stack_pop();
+                    let a = self.stack_pop();
+                    let result = arithmetic_op!(&a, &b, -).with_span(&frame, self)?;
+                    self.stack.push(result);
+                }
+
+                OpCode::OpMul => {
+                    let b = self.stack_pop();
+                    let a = self.stack_pop();
+                    let result = arithmetic_op!(&a, &b, *).with_span(&frame, self)?;
+                    self.stack.push(result);
+                }
+
+                OpCode::OpDiv => {
+                    let b = self.stack_pop();
+
+                    match b {
+                        Value::Integer(0) => return frame.error(self, ErrorKind::DivisionByZero),
+                        Value::Float(b) if b == 0.0_f64 => {
+                            return frame.error(self, ErrorKind::DivisionByZero)
+                        }
+                        _ => {}
+                    };
+
+                    let a = self.stack_pop();
+                    let result = arithmetic_op!(&a, &b, /).with_span(&frame, self)?;
+                    self.stack.push(result);
+                }
+
+                OpCode::OpNegate => match self.stack_pop() {
+                    Value::Integer(i) => self.stack.push(Value::Integer(-i)),
+                    Value::Float(f) => self.stack.push(Value::Float(-f)),
+                    v => {
+                        return frame.error(
+                            self,
+                            ErrorKind::TypeError {
+                                expected: "number (either int or float)",
+                                actual: v.type_of(),
+                            },
+                        );
+                    }
+                },
+
+                OpCode::OpLess => cmp_op!(self, frame, span, <),
+                OpCode::OpLessOrEq => cmp_op!(self, frame, span, <=),
+                OpCode::OpMore => cmp_op!(self, frame, span, >),
+                OpCode::OpMoreOrEq => cmp_op!(self, frame, span, >=),
+
+                OpCode::OpFindFile => match self.stack_pop() {
+                    Value::UnresolvedPath(path) => {
+                        let resolved = self
+                            .nix_search_path
+                            .resolve(&mut *self.io_handle, *path)
+                            .with_span(&frame, self)?;
+                        self.stack.push(resolved.into());
+                    }
+
+                    _ => panic!("tvix compiler bug: OpFindFile called on non-UnresolvedPath"),
+                },
+
+                OpCode::OpResolveHomePath => match self.stack_pop() {
+                    Value::UnresolvedPath(path) => {
+                        match dirs::home_dir() {
+                            None => {
+                                return frame.error(
+                                    self,
+                                    ErrorKind::RelativePathResolution(
+                                        "failed to determine home directory".into(),
+                                    ),
+                                );
+                            }
+                            Some(mut buf) => {
+                                buf.push(*path);
+                                self.stack.push(buf.into());
+                            }
+                        };
+                    }
+
+                    _ => {
+                        panic!("tvix compiler bug: OpResolveHomePath called on non-UnresolvedPath")
+                    }
+                },
+
+                OpCode::OpPushWith(StackIdx(idx)) => self.with_stack.push(frame.stack_offset + idx),
+
+                OpCode::OpPopWith => {
+                    self.with_stack.pop();
+                }
+
+                OpCode::OpAssertFail => {
+                    frame.error(self, ErrorKind::AssertionFailed)?;
+                }
+
+                // Data-carrying operands should never be executed,
+                // that is a critical error in the VM/compiler.
+                OpCode::DataStackIdx(_)
+                | OpCode::DataDeferredLocal(_)
+                | OpCode::DataUpvalueIdx(_)
+                | OpCode::DataCaptureWith => {
+                    panic!("Tvix bug: attempted to execute data-carrying operand")
+                }
+            }
+        }
+    }
+}
+
+/// Implementation of helper functions for the runtime logic above.
+impl<'o> VM<'o> {
+    pub(crate) fn stack_pop(&mut self) -> Value {
+        self.stack.pop().expect("runtime stack empty")
+    }
+
+    fn stack_peek(&self, offset: usize) -> &Value {
+        &self.stack[self.stack.len() - 1 - offset]
+    }
+
+    fn run_attrset(&mut self, frame: &CallFrame, count: usize) -> EvalResult<()> {
+        let attrs = NixAttrs::construct(count, self.stack.split_off(self.stack.len() - count * 2))
+            .with_span(frame, self)?;
+
+        self.stack.push(Value::attrs(attrs));
+        Ok(())
+    }
+
+    /// Access the last call frame present in the frame stack.
+    fn last_call_frame(&self) -> Option<&CallFrame> {
+        for frame in self.frames.iter().rev() {
+            if let Frame::CallFrame { call_frame, .. } = frame {
+                return Some(call_frame);
+            }
+        }
+
+        None
+    }
+
+    /// Push an already constructed warning.
+    pub fn push_warning(&mut self, warning: EvalWarning) {
+        self.warnings.push(warning);
+    }
+
+    /// Emit a warning with the given WarningKind and the source span
+    /// of the current instruction.
+    pub fn emit_warning(&mut self, _kind: WarningKind) {
+        // TODO: put LightSpan in warning, calculate only *after* eval
+        // TODO: what to do with the spans?
+        // self.push_warning(EvalWarning {
+        //     kind,
+        //     span: self.current_span(),
+        // });
+    }
+
+    /// Interpolate string fragments by popping the specified number of
+    /// fragments of the stack, evaluating them to strings, and pushing
+    /// the concatenated result string back on the stack.
+    fn run_interpolate(&mut self, frame: &CallFrame, count: usize) -> EvalResult<()> {
+        let mut out = String::new();
+
+        for _ in 0..count {
+            out.push_str(self.stack_pop().to_str().with_span(frame, self)?.as_str());
+        }
+
+        self.stack.push(Value::String(out.into()));
+        Ok(())
+    }
+
+    /// Returns a reasonable light span for the current situation that the VM is
+    /// in.
+    pub fn reasonable_light_span(&self) -> LightSpan {
+        self.reasonable_span.clone()
+    }
+
+    /// Apply an argument from the stack to a builtin, and attempt to call it.
+    ///
+    /// All calls are tail-calls in Tvix, as every function application is a
+    /// separate thunk and OpCall is thus the last result in the thunk.
+    ///
+    /// Due to this, once control flow exits this function, the generator will
+    /// automatically be run by the VM.
+    fn call_builtin(&mut self, span: LightSpan, mut builtin: Builtin) -> EvalResult<()> {
+        let builtin_name = builtin.name();
+        self.observer.observe_enter_builtin(builtin_name);
+
+        builtin.apply_arg(self.stack_pop());
+
+        match builtin.call() {
+            // Partially applied builtin is just pushed back on the stack.
+            BuiltinResult::Partial(partial) => self.stack.push(Value::Builtin(partial)),
+
+            // Builtin is fully applied and the generator needs to be run by the VM.
+            BuiltinResult::Called(name, generator) => self.frames.push(Frame::Generator {
+                generator,
+                span,
+                name,
+                state: GeneratorState::Running,
+            }),
+        }
+
+        Ok(())
+    }
+
+    fn call_value(
+        &mut self,
+        span: LightSpan,
+        parent: Option<(LightSpan, CallFrame)>,
+        callable: Value,
+    ) -> EvalResult<()> {
+        match callable {
+            Value::Builtin(builtin) => self.call_builtin(span, builtin),
+            Value::Thunk(thunk) => self.call_value(span, parent, thunk.value().clone()),
+
+            Value::Closure(closure) => {
+                let lambda = closure.lambda();
+                self.observer.observe_tail_call(self.frames.len(), &lambda);
+
+                // The stack offset is always `stack.len() - arg_count`, and
+                // since this branch handles native Nix functions (which always
+                // take only a single argument and are curried), the offset is
+                // `stack_len - 1`.
+                let stack_offset = self.stack.len() - 1;
+
+                // Reenqueue the parent frame, which should only have
+                // `OpReturn` left. Not throwing it away leads to more
+                // useful error traces.
+                if let Some((parent_span, parent_frame)) = parent {
+                    self.push_call_frame(parent_span, parent_frame);
+                }
+
+                self.push_call_frame(
+                    span,
+                    CallFrame {
+                        lambda,
+                        upvalues: closure.upvalues(),
+                        ip: CodeIdx(0),
+                        stack_offset,
+                    },
+                );
+
+                Ok(())
+            }
+
+            // Attribute sets with a __functor attribute are callable.
+            val @ Value::Attrs(_) => {
+                if let Some((parent_span, parent_frame)) = parent {
+                    self.push_call_frame(parent_span, parent_frame);
+                }
+
+                self.enqueue_generator("__functor call", span, |co| call_functor(co, val));
+                Ok(())
+            }
+            v => Err(ErrorKind::NotCallable(v.type_of())).with_span(&span, self),
+        }
+    }
+
+    /// Populate the upvalue fields of a thunk or closure under construction.
+    fn populate_upvalues(
+        &mut self,
+        frame: &mut CallFrame,
+        count: usize,
+        mut upvalues: impl DerefMut<Target = Upvalues>,
+    ) -> EvalResult<()> {
+        for _ in 0..count {
+            match frame.inc_ip() {
+                OpCode::DataStackIdx(StackIdx(stack_idx)) => {
+                    let idx = frame.stack_offset + stack_idx;
+
+                    let val = match self.stack.get(idx) {
+                        Some(val) => val.clone(),
+                        None => {
+                            return frame.error(
+                                self,
+                                ErrorKind::TvixBug {
+                                    msg: "upvalue to be captured was missing on stack",
+                                    metadata: Some(Rc::new(json!({
+                                        "ip": format!("{:#x}", frame.ip.0 - 1),
+                                        "stack_idx(relative)": stack_idx,
+                                        "stack_idx(absolute)": idx,
+                                    }))),
+                                },
+                            );
+                        }
+                    };
+
+                    upvalues.deref_mut().push(val);
+                }
+
+                OpCode::DataUpvalueIdx(upv_idx) => {
+                    upvalues.deref_mut().push(frame.upvalue(upv_idx).clone());
+                }
+
+                OpCode::DataDeferredLocal(idx) => {
+                    upvalues.deref_mut().push(Value::DeferredUpvalue(idx));
+                }
+
+                OpCode::DataCaptureWith => {
+                    // Start the captured with_stack off of the
+                    // current call frame's captured with_stack, ...
+                    let mut captured_with_stack = frame
+                        .upvalues
+                        .with_stack()
+                        .map(Clone::clone)
+                        // ... or make an empty one if there isn't one already.
+                        .unwrap_or_else(|| Vec::with_capacity(self.with_stack.len()));
+
+                    for idx in &self.with_stack {
+                        captured_with_stack.push(self.stack[*idx].clone());
+                    }
+
+                    upvalues.deref_mut().set_with_stack(captured_with_stack);
+                }
+
+                _ => panic!("compiler error: missing closure operand"),
+            }
+        }
+
+        Ok(())
+    }
+}
+
+// TODO(amjoseph): de-asyncify this
+/// Resolve a dynamically bound identifier (through `with`) by looking
+/// for matching values in the with-stacks carried at runtime.
+async fn resolve_with(
+    co: GenCo,
+    ident: String,
+    vm_with_len: usize,
+    upvalue_with_len: usize,
+) -> Result<Value, ErrorKind> {
+    /// Fetch and force a value on the with-stack from the VM.
+    async fn fetch_forced_with(co: &GenCo, idx: usize) -> Value {
+        match co.yield_(VMRequest::WithValue(idx)).await {
+            VMResponse::Value(value) => value,
+            msg => panic!(
+                "Tvix bug: VM responded with incorrect generator message: {}",
+                msg
+            ),
+        }
+    }
+
+    /// Fetch and force a value on the *captured* with-stack from the VM.
+    async fn fetch_captured_with(co: &GenCo, idx: usize) -> Value {
+        match co.yield_(VMRequest::CapturedWithValue(idx)).await {
+            VMResponse::Value(value) => value,
+            msg => panic!(
+                "Tvix bug: VM responded with incorrect generator message: {}",
+                msg
+            ),
+        }
+    }
+
+    for with_stack_idx in (0..vm_with_len).rev() {
+        // TODO(tazjin): is this branch still live with the current with-thunking?
+        let with = fetch_forced_with(&co, with_stack_idx).await;
+
+        match with.to_attrs()?.select(&ident) {
+            None => continue,
+            Some(val) => return Ok(val.clone()),
+        }
+    }
+
+    for upvalue_with_idx in (0..upvalue_with_len).rev() {
+        let with = fetch_captured_with(&co, upvalue_with_idx).await;
+
+        match with.to_attrs()?.select(&ident) {
+            None => continue,
+            Some(val) => return Ok(val.clone()),
+        }
+    }
+
+    Err(ErrorKind::UnknownDynamicVariable(ident))
+}
+
+// TODO(amjoseph): de-asyncify this
+async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> {
+    let result = match (a, b) {
+        (Value::Path(p), v) => {
+            let mut path = p.to_string_lossy().into_owned();
+            let vs = generators::request_string_coerce(&co, v, CoercionKind::Weak).await;
+            path.push_str(vs.as_str());
+            crate::value::canon_path(PathBuf::from(path)).into()
+        }
+        (Value::String(s1), Value::String(s2)) => Value::String(s1.concat(&s2)),
+        (Value::String(s1), v) => Value::String(
+            s1.concat(&generators::request_string_coerce(&co, v, CoercionKind::Weak).await),
+        ),
+        (v, Value::String(s2)) => Value::String(
+            generators::request_string_coerce(&co, v, CoercionKind::Weak)
+                .await
+                .concat(&s2),
+        ),
+        (a, b) => arithmetic_op!(&a, &b, +)?,
+    };
+
+    Ok(result)
+}
+
+/// The result of a VM's runtime evaluation.
+pub struct RuntimeResult {
+    pub value: Value,
+    pub warnings: Vec<EvalWarning>,
+}
+
+// TODO(amjoseph): de-asyncify this
+/// Generator that retrieves the final value from the stack, and deep-forces it
+/// before returning.
+async fn final_deep_force(co: GenCo) -> Result<Value, ErrorKind> {
+    let value = generators::request_stack_pop(&co).await;
+    Ok(generators::request_deep_force(&co, value, SharedThunkSet::default()).await)
+}
+
+pub fn run_lambda(
+    nix_search_path: NixSearchPath,
+    io_handle: Box<dyn EvalIO>,
+    observer: &mut dyn RuntimeObserver,
+    globals: Rc<GlobalsMap>,
+    lambda: Rc<Lambda>,
+    strict: bool,
+) -> EvalResult<RuntimeResult> {
+    // Retain the top-level span of the expression in this lambda, as
+    // synthetic "calls" in deep_force will otherwise not have a span
+    // to fall back to.
+    //
+    // We exploit the fact that the compiler emits a final instruction
+    // with the span of the entire file for top-level expressions.
+    let root_span = lambda.chunk.get_span(CodeIdx(lambda.chunk.code.len() - 1));
+
+    let mut vm = VM::new(
+        nix_search_path,
+        io_handle,
+        observer,
+        globals,
+        root_span.into(),
+    );
+
+    // When evaluating strictly, synthesise a frame that will instruct
+    // the VM to deep-force the final value before returning it.
+    if strict {
+        vm.enqueue_generator("final_deep_force", root_span.into(), final_deep_force);
+    }
+
+    vm.frames.push(Frame::CallFrame {
+        span: root_span.into(),
+        call_frame: CallFrame {
+            lambda,
+            upvalues: Rc::new(Upvalues::with_capacity(0)),
+            ip: CodeIdx(0),
+            stack_offset: 0,
+        },
+    });
+
+    vm.execute()
+}
diff --git a/tvix/eval/src/warnings.rs b/tvix/eval/src/warnings.rs
new file mode 100644
index 000000000000..aa567f302755
--- /dev/null
+++ b/tvix/eval/src/warnings.rs
@@ -0,0 +1,142 @@
+//! Implements warnings that are emitted in cases where code passed to
+//! Tvix exhibits problems that the user could address.
+
+use codemap_diagnostic::{ColorConfig, Diagnostic, Emitter, Level, SpanLabel, SpanStyle};
+
+use crate::SourceCode;
+
+#[derive(Debug)]
+pub enum WarningKind {
+    DeprecatedLiteralURL,
+    UselessInherit,
+    UnusedBinding,
+    ShadowedGlobal(&'static str),
+    DeprecatedLegacyLet,
+    InvalidNixPath(String),
+    UselessBoolOperation(&'static str),
+    DeadCode,
+    EmptyInherit,
+    EmptyLet,
+
+    /// Tvix internal warning for features triggered by users that are
+    /// not actually implemented yet, but do not cause runtime failures.
+    NotImplemented(&'static str),
+}
+
+#[derive(Debug)]
+pub struct EvalWarning {
+    pub kind: WarningKind,
+    pub span: codemap::Span,
+}
+
+impl EvalWarning {
+    /// Render a fancy, human-readable output of this warning and
+    /// return it as a String. Note that this version of the output
+    /// does not include any colours or font styles.
+    pub fn fancy_format_str(&self, source: &SourceCode) -> String {
+        let mut out = vec![];
+        Emitter::vec(&mut out, Some(&*source.codemap())).emit(&[self.diagnostic(source)]);
+        String::from_utf8_lossy(&out).to_string()
+    }
+
+    /// Render a fancy, human-readable output of this warning and
+    /// print it to stderr. If rendered in a terminal that supports
+    /// colours and font styles, the output will include those.
+    pub fn fancy_format_stderr(&self, source: &SourceCode) {
+        Emitter::stderr(ColorConfig::Auto, Some(&*source.codemap()))
+            .emit(&[self.diagnostic(source)]);
+    }
+
+    /// Create the optional span label displayed as an annotation on
+    /// the underlined span of the warning.
+    fn span_label(&self) -> Option<String> {
+        match self.kind {
+            WarningKind::UnusedBinding | WarningKind::ShadowedGlobal(_) => {
+                Some("variable declared here".into())
+            }
+            _ => None,
+        }
+    }
+
+    /// Create the primary warning message displayed to users for a
+    /// warning.
+    fn message(&self, source: &SourceCode) -> String {
+        match self.kind {
+            WarningKind::DeprecatedLiteralURL => {
+                "URL literal syntax is deprecated, use a quoted string instead".to_string()
+            }
+
+            WarningKind::UselessInherit => {
+                "inherit does nothing (this variable already exists with the same value)"
+                    .to_string()
+            }
+
+            WarningKind::UnusedBinding => {
+                format!(
+                    "variable '{}' is declared, but never used:",
+                    source.source_slice(self.span)
+                )
+            }
+
+            WarningKind::ShadowedGlobal(name) => {
+                format!("declared variable '{}' shadows a built-in global!", name)
+            }
+
+            WarningKind::DeprecatedLegacyLet => {
+                "legacy `let` syntax used, please rewrite this as `let .. in ...`".to_string()
+            }
+
+            WarningKind::InvalidNixPath(ref err) => {
+                format!("invalid NIX_PATH resulted in a parse error: {}", err)
+            }
+
+            WarningKind::UselessBoolOperation(msg) => {
+                format!("useless operation on boolean: {}", msg)
+            }
+
+            WarningKind::DeadCode => "this code will never be executed".to_string(),
+
+            WarningKind::EmptyInherit => "this `inherit` statement is empty".to_string(),
+
+            WarningKind::EmptyLet => "this `let`-expression contains no bindings".to_string(),
+
+            WarningKind::NotImplemented(what) => {
+                format!("feature not yet implemented in tvix: {}", what)
+            }
+        }
+    }
+
+    /// Return the unique warning code for this variant which can be
+    /// used to refer users to documentation.
+    fn code(&self) -> &'static str {
+        match self.kind {
+            WarningKind::DeprecatedLiteralURL => "W001",
+            WarningKind::UselessInherit => "W002",
+            WarningKind::UnusedBinding => "W003",
+            WarningKind::ShadowedGlobal(_) => "W004",
+            WarningKind::DeprecatedLegacyLet => "W005",
+            WarningKind::InvalidNixPath(_) => "W006",
+            WarningKind::UselessBoolOperation(_) => "W007",
+            WarningKind::DeadCode => "W008",
+            WarningKind::EmptyInherit => "W009",
+            WarningKind::EmptyLet => "W010",
+
+            WarningKind::NotImplemented(_) => "W999",
+        }
+    }
+
+    fn diagnostic(&self, source: &SourceCode) -> Diagnostic {
+        let span_label = SpanLabel {
+            label: self.span_label(),
+            span: self.span,
+            style: SpanStyle::Primary,
+        };
+
+        Diagnostic {
+            level: Level::Warning,
+            message: self.message(source),
+            spans: vec![span_label],
+            code: Some(self.code().into()),
+        }
+    }
+}
diff --git a/tvix/eval/tests/nix_oracle.rs b/tvix/eval/tests/nix_oracle.rs
new file mode 100644
index 000000000000..ecaf53a1b21a
--- /dev/null
+++ b/tvix/eval/tests/nix_oracle.rs
@@ -0,0 +1,157 @@
+//! Tests which use upstream nix as an oracle to test evaluation against
+
+use std::{env, path::PathBuf, process::Command};
+
+use pretty_assertions::assert_eq;
+use tempdir::TempDir;
+
+fn nix_binary_path() -> PathBuf {
+    env::var("NIX_INSTANTIATE_BINARY_PATH")
+        .unwrap_or_else(|_| "nix-instantiate".to_owned())
+        .into()
+}
+
+#[derive(Clone, Copy)]
+enum Strictness {
+    Lazy,
+    Strict,
+}
+
+fn nix_eval(expr: &str, strictness: Strictness) -> String {
+    let store_dir = TempDir::new("store-dir").unwrap();
+
+    let mut args = match strictness {
+        Strictness::Lazy => vec![],
+        Strictness::Strict => vec!["--strict"],
+    };
+    args.extend_from_slice(&["--eval", "-E"]);
+
+    let output = Command::new(nix_binary_path())
+        .args(&args[..])
+        .arg(format!("({expr})"))
+        .env(
+            "NIX_REMOTE",
+            format!("local?root={}", store_dir.path().display()),
+        )
+        .output()
+        .unwrap();
+    if !output.status.success() {
+        panic!(
+            "nix eval {expr} failed!\n    stdout: {}\n    stderr: {}",
+            String::from_utf8_lossy(&output.stdout),
+            String::from_utf8_lossy(&output.stderr)
+        )
+    }
+
+    String::from_utf8(output.stdout).unwrap()
+}
+
+/// Compare the evaluation of the given nix expression in nix (using the
+/// `NIX_INSTANTIATE_BINARY_PATH` env var to resolve the `nix-instantiate` binary) and tvix, and
+/// assert that the result is identical
+#[track_caller]
+fn compare_eval(expr: &str, strictness: Strictness) {
+    let nix_result = nix_eval(expr, strictness);
+    let mut eval = tvix_eval::Evaluation::new(expr, None);
+    eval.strict = matches!(strictness, Strictness::Strict);
+    eval.io_handle = Box::new(tvix_eval::StdIO);
+
+    let tvix_result = eval
+        .evaluate()
+        .value
+        .expect("tvix evaluation should succeed")
+        .to_string();
+
+    assert_eq!(nix_result.trim(), tvix_result);
+}
+
+/// Generate a suite of tests which call [`compare_eval`] on expressions, checking that nix and tvix
+/// return identical results.
+macro_rules! compare_eval_tests {
+    ($strictness:expr, {}) => {};
+    ($strictness:expr, {$(#[$meta:meta])* $test_name: ident($expr: expr); $($rest:tt)*}) => {
+        #[test]
+        $(#[$meta])*
+        fn $test_name() {
+            compare_eval($expr, $strictness);
+        }
+
+        compare_eval_tests!($strictness, { $($rest)* });
+    }
+}
+
+macro_rules! compare_strict_eval_tests {
+    ($($tests:tt)*) => {
+        compare_eval_tests!(Strictness::Lazy, { $($tests)* });
+    }
+}
+
+macro_rules! compare_lazy_eval_tests {
+    ($($tests:tt)*) => {
+        compare_eval_tests!(Strictness::Lazy, { $($tests)* });
+    }
+}
+
+compare_strict_eval_tests! {
+    literal_int("1");
+    add_ints("1 + 1");
+    add_lists("[1 2] ++ [3 4]");
+    add_paths(r#"[
+        (./. + "/")
+        (./foo + "bar")
+        (let name = "bar"; in ./foo + name)
+        (let name = "bar"; in ./foo + "${name}")
+        (let name = "bar"; in ./foo + "/" + "${name}")
+        (let name = "bar"; in ./foo + "/${name}")
+        (./. + ./.)
+    ]"#);
+}
+
+// TODO(sterni): tvix_tests should gain support for something similar in the future,
+// but this requires messing with the path naming which would break compat with
+// C++ Nix's test suite
+compare_lazy_eval_tests! {
+    // Wrap every expression type supported by [Compiler::compile] in a list
+    // with lazy evaluation enabled, so we can check it being thunked or not
+    // against C++ Nix.
+    unthunked_literals_in_list("[ https://tvl.fyi 1 1.2 ]");
+    unthunked_path_in_list("[ ./nix_oracle.rs ]");
+    unthunked_string_literal_in_list("[ \":thonking:\" ]");
+    thunked_unary_ops_in_list("[ (!true) (-1) ]");
+    thunked_bin_ops_in_list(r#"
+      let
+        # Necessary to fool the optimiser for && and ||
+        true' = true;
+        false' = false;
+      in
+      [
+        (true' && false')
+        (true' || false')
+        (false -> true)
+        (40 + 2)
+        (43 - 1)
+        (21 * 2)
+        (126 / 3)
+        ({ } // { bar = null; })
+        (12 == 13)
+        (3 < 2)
+        (4 > 2)
+        (23 >= 42)
+        (33 <= 22)
+        ([ ] ++ [ ])
+        (42 != null)
+      ]
+    "#);
+    thunked_has_attrs_in_list("[ ({ } ? foo) ]");
+    thunked_list_in_list("[ [ 1 2 3 ] ]");
+    thunked_attr_set_in_list("[ { foo = null; } ]");
+    thunked_select_in_list("[ ({ foo = null; }.bar) ]");
+    thunked_assert_in_list("[ (assert false; 12) ]");
+    thunked_if_in_list("[ (if false then 13 else 12) ]");
+    thunked_let_in_list("[ (let foo = 12; in foo) ]");
+    thunked_with_in_list("[ (with { foo = 13; }; fooo) ]");
+    unthunked_identifier_in_list("let foo = 12; in [ foo ]");
+    thunked_lambda_in_list("[ (x: x) ]");
+    thunked_function_application_in_list("[ (builtins.add 1 2) ]");
+    thunked_legacy_let_in_list("[ (let { foo = 12; body = foo; }) ]");
+}
diff --git a/tvix/nix-compat/Cargo.toml b/tvix/nix-compat/Cargo.toml
new file mode 100644
index 000000000000..49ddbf4728e9
--- /dev/null
+++ b/tvix/nix-compat/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "nix-compat"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0.68"
+data-encoding = "2.3.3"
+glob = "0.3.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+sha2 = "0.10.6"
+thiserror = "1.0.38"
+
+[dev-dependencies]
+serde_json = "1.0"
+test-case = "2.2.2"
+
+[dev-dependencies.test-generator]
+# This fork of test-generator adds support for cargo workspaces, see
+# also https://github.com/frehberg/test-generator/pull/14
+git = "https://github.com/JamesGuthrie/test-generator.git"
+rev = "82e799979980962aec1aa324ec6e0e4cad781f41"
diff --git a/tvix/nix-compat/default.nix b/tvix/nix-compat/default.nix
new file mode 100644
index 000000000000..7f51438eb70d
--- /dev/null
+++ b/tvix/nix-compat/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.tvix.crates.workspaceMembers.nix-compat.build.override {
+  runTests = true;
+}
diff --git a/tvix/nix-compat/src/derivation/errors.rs b/tvix/nix-compat/src/derivation/errors.rs
new file mode 100644
index 000000000000..8e9e6a121096
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/errors.rs
@@ -0,0 +1,59 @@
+use crate::{nixbase32::Nixbase32DecodeError, store_path};
+use thiserror::Error;
+
+/// Errors that can occur during the validation of Derivation structs.
+#[derive(Debug, Error, PartialEq)]
+pub enum DerivationError {
+    // outputs
+    #[error("no outputs defined")]
+    NoOutputs(),
+    #[error("invalid output name: {0}")]
+    InvalidOutputName(String),
+    #[error("encountered fixed-output derivation, but more than 1 output in total")]
+    MoreThanOneOutputButFixed(),
+    #[error("invalid output name for fixed-output derivation: {0}")]
+    InvalidOutputNameForFixed(String),
+    #[error("unable to validate output {0}: {1}")]
+    InvalidOutput(String, OutputError),
+    #[error("unable to validate output {0}: {1}")]
+    InvalidOutputDerivationPath(String, store_path::BuildStorePathError),
+    // input derivation
+    #[error("unable to parse input derivation path {0}: {1}")]
+    InvalidInputDerivationPath(String, store_path::Error),
+    #[error("input derivation {0} doesn't end with .drv")]
+    InvalidInputDerivationPrefix(String),
+    #[error("input derivation {0} output names are empty")]
+    EmptyInputDerivationOutputNames(String),
+    #[error("input derivation {0} output name {1} is invalid")]
+    InvalidInputDerivationOutputName(String, String),
+
+    // input sources
+    #[error("unable to parse input sources path {0}: {1}")]
+    InvalidInputSourcesPath(String, store_path::Error),
+
+    // platform
+    #[error("invalid platform field: {0}")]
+    InvalidPlatform(String),
+
+    // builder
+    #[error("invalid builder field: {0}")]
+    InvalidBuilder(String),
+
+    // environment
+    #[error("invalid environment key {0}")]
+    InvalidEnvironmentKey(String),
+}
+
+/// Errors that can occur during the validation of a specific
+// [crate::derivation::Output] of a [crate::derivation::Derviation].
+#[derive(Debug, Error, PartialEq)]
+pub enum OutputError {
+    #[error("Invalid output path {0}: {1}")]
+    InvalidOutputPath(String, store_path::Error),
+    #[error("Invalid hash encoding: {0}")]
+    InvalidHashEncoding(String, Nixbase32DecodeError),
+    #[error("Invalid hash algo: {0}")]
+    InvalidHashAlgo(String),
+    #[error("Invalid Digest size {0} for algo {1}")]
+    InvalidDigestSizeForAlgo(usize, String),
+}
diff --git a/tvix/nix-compat/src/derivation/mod.rs b/tvix/nix-compat/src/derivation/mod.rs
new file mode 100644
index 000000000000..ab1471165549
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/mod.rs
@@ -0,0 +1,240 @@
+use crate::store_path::{
+    self, build_output_path, build_regular_ca_path, build_text_path, StorePath,
+};
+use serde::{Deserialize, Serialize};
+use sha2::{Digest, Sha256};
+use std::collections::{BTreeMap, BTreeSet};
+
+mod errors;
+mod output;
+mod string_escape;
+mod validate;
+mod write;
+
+#[cfg(test)]
+mod tests;
+
+// Public API of the crate.
+pub use crate::nixhash::{NixHash, NixHashWithMode};
+pub use errors::{DerivationError, OutputError};
+pub use output::Output;
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+pub struct Derivation {
+    #[serde(rename = "args")]
+    pub arguments: Vec<String>,
+
+    pub builder: String,
+
+    #[serde(rename = "env")]
+    pub environment: BTreeMap<String, String>,
+
+    #[serde(rename = "inputDrvs")]
+    pub input_derivations: BTreeMap<String, BTreeSet<String>>,
+
+    #[serde(rename = "inputSrcs")]
+    pub input_sources: BTreeSet<String>,
+
+    pub outputs: BTreeMap<String, Output>,
+
+    pub system: String,
+}
+
+impl Derivation {
+    /// write the Derivation to the given [std::fmt::Write], in ATerm format.
+    ///
+    /// The only errors returns are these when writing to the passed writer.
+    pub fn serialize(&self, writer: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
+        writer.write_str(write::DERIVATION_PREFIX)?;
+        writer.write_char(write::PAREN_OPEN)?;
+
+        write::write_outputs(writer, &self.outputs)?;
+        write::write_input_derivations(writer, &self.input_derivations)?;
+        write::write_input_sources(writer, &self.input_sources)?;
+        write::write_system(writer, &self.system)?;
+        write::write_builder(writer, &self.builder)?;
+        write::write_arguments(writer, &self.arguments)?;
+        write::write_enviroment(writer, &self.environment)?;
+
+        writer.write_char(write::PAREN_CLOSE)?;
+
+        Ok(())
+    }
+
+    /// return the ATerm serialization as a string.
+    pub fn to_aterm_string(&self) -> String {
+        let mut buffer = String::new();
+
+        // invoke serialize and write to the buffer.
+        // Note we only propagate errors writing to the writer in serialize,
+        // which won't panic for the string we write to.
+        self.serialize(&mut buffer).unwrap();
+
+        buffer
+    }
+
+    /// Returns the drv path of a [Derivation] struct.
+    ///
+    /// The drv path is calculated by invoking [build_text_path], using
+    /// the `name` with a `.drv` suffix as name, all [Derivation::input_sources] and
+    /// keys of [Derivation::input_derivations] as references, and the ATerm string of
+    /// the [Derivation] as content.
+    pub fn calculate_derivation_path(&self, name: &str) -> Result<StorePath, DerivationError> {
+        // append .drv to the name
+        let name = &format!("{}.drv", name);
+
+        // collect the list of paths from input_sources and input_derivations
+        // into a (sorted, guaranteed by BTreeSet) list of references
+        let references: BTreeSet<String> = {
+            let mut inputs = self.input_sources.clone();
+            let input_derivation_keys: Vec<String> =
+                self.input_derivations.keys().cloned().collect();
+            inputs.extend(input_derivation_keys);
+            inputs
+        };
+
+        build_text_path(name, self.to_aterm_string(), references)
+            .map_err(|_e| DerivationError::InvalidOutputName(name.to_string()))
+    }
+
+    /// Returns the FOD digest, if the derivation is fixed-output, or None if
+    /// it's not.
+    fn fod_digest(&self) -> Option<Vec<u8>> {
+        if self.outputs.len() != 1 {
+            return None;
+        }
+
+        let out_output = self.outputs.get("out")?;
+        Some(
+            Sha256::new_with_prefix(format!(
+                "fixed:out:{}:{}",
+                out_output.hash_with_mode.clone()?.to_nix_hash_string(),
+                out_output.path
+            ))
+            .finalize()
+            .to_vec(),
+        )
+    }
+
+    /// Calculates the hash of a derivation modulo fixed-output subderivations.
+    ///
+    /// This is called `hashDerivationModulo` in nixcpp.
+    ///
+    /// It returns a [NixHash], created by calculating the sha256 digest of
+    /// the derivation ATerm representation, except that:
+    ///  -  any input derivation paths have beed replaced "by the result of a
+    ///     recursive call to this function" and that
+    ///  - for fixed-output derivations the special
+    ///    `fixed:out:${algo}:${digest}:${fodPath}` string is hashed instead of
+    ///    the A-Term.
+    ///
+    /// If the derivation is not a fixed derivation, it's up to the caller of
+    /// this function to provide a lookup function to lookup these calculation
+    /// results of parent derivations at `fn_get_hash_derivation_modulo` (by
+    /// drv path).
+    pub fn derivation_or_fod_hash<F>(&self, fn_get_derivation_or_fod_hash: F) -> NixHash
+    where
+        F: Fn(&str) -> NixHash,
+    {
+        // Fixed-output derivations return a fixed hash.
+        // Non-Fixed-output derivations return a hash of the ATerm notation, but with all
+        // input_derivation paths replaced by a recursive call to this function.
+        // We use fn_get_derivation_or_fod_hash here, so callers can precompute this.
+        let digest = self.fod_digest().unwrap_or({
+            // This is a new map from derivation_or_fod_hash.digest (as lowerhex)
+            // to list of output names
+            let mut replaced_input_derivations: BTreeMap<String, BTreeSet<String>> =
+                BTreeMap::new();
+
+            // For each input_derivation, look up the
+            // derivation_or_fod_hash, and replace the derivation path with it's HEXLOWER
+            // digest.
+            // This is not the [NixHash::to_nix_hash_string], but without the sha256: prefix).
+            for (drv_path, output_names) in &self.input_derivations {
+                replaced_input_derivations.insert(
+                    data_encoding::HEXLOWER.encode(&fn_get_derivation_or_fod_hash(drv_path).digest),
+                    output_names.clone(),
+                );
+            }
+
+            // construct a new derivation struct with these replaced input derivation strings
+            let replaced_derivation = Derivation {
+                input_derivations: replaced_input_derivations,
+                ..self.clone()
+            };
+
+            // write the ATerm of that to the hash function
+            let mut hasher = Sha256::new();
+            hasher.update(replaced_derivation.to_aterm_string());
+
+            hasher.finalize().to_vec()
+        });
+        NixHash::new(crate::nixhash::HashAlgo::Sha256, digest.to_vec())
+    }
+
+    /// This calculates all output paths of a Derivation and updates the struct.
+    /// It requires the struct to be initially without output paths.
+    /// This means, self.outputs[$outputName].path needs to be an empty string,
+    /// and self.environment[$outputName] needs to be an empty string.
+    ///
+    /// Output path calculation requires knowledge of the
+    /// derivation_or_fod_hash [NixHash], which (in case of non-fixed-output
+    /// derivations) also requires knowledge of other hash_derivation_modulo
+    /// [NixHash]es.
+    ///
+    /// We solve this by asking the caller of this function to provide the
+    /// hash_derivation_modulo of the current Derivation.
+    ///
+    /// On completion, self.environment[$outputName] and
+    /// self.outputs[$outputName].path are set to the calculated output path for all
+    /// outputs.
+    pub fn calculate_output_paths(
+        &mut self,
+        name: &str,
+        derivation_or_fod_hash: &NixHash,
+    ) -> Result<(), DerivationError> {
+        // The fingerprint and hash differs per output
+        for (output_name, output) in self.outputs.iter_mut() {
+            // Assert that outputs are not yet populated, to avoid using this function wrongly.
+            // We don't also go over self.environment, but it's a sufficient
+            // footgun prevention mechanism.
+            assert!(output.path.is_empty());
+
+            let path_name = output_path_name(name, output_name);
+
+            // For fixed output derivation we use the per-output info, otherwise we use the
+            // derivation hash.
+            let abs_store_path = if let Some(ref hwm) = output.hash_with_mode {
+                build_regular_ca_path(&path_name, hwm, Vec::<String>::new(), false).map_err(
+                    |e| DerivationError::InvalidOutputDerivationPath(output_name.to_string(), e),
+                )?
+            } else {
+                build_output_path(derivation_or_fod_hash, output_name, &path_name).map_err(|e| {
+                    DerivationError::InvalidOutputDerivationPath(
+                        output_name.to_string(),
+                        store_path::BuildStorePathError::InvalidName(e),
+                    )
+                })?
+            };
+
+            output.path = abs_store_path.to_absolute_path();
+            self.environment
+                .insert(output_name.to_string(), abs_store_path.to_absolute_path());
+        }
+
+        Ok(())
+    }
+}
+
+/// Calculate the name part of the store path of a derivation [Output].
+///
+/// It's the name, and (if it's the non-out output), the output name
+/// after a `-`.
+fn output_path_name(derivation_name: &str, output_name: &str) -> String {
+    let mut output_path_name = derivation_name.to_string();
+    if output_name != "out" {
+        output_path_name.push('-');
+        output_path_name.push_str(output_name);
+    }
+    output_path_name
+}
diff --git a/tvix/nix-compat/src/derivation/output.rs b/tvix/nix-compat/src/derivation/output.rs
new file mode 100644
index 000000000000..4bfc7bf8014d
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/output.rs
@@ -0,0 +1,36 @@
+use crate::derivation::OutputError;
+use crate::nixhash::{HashAlgo, NixHashWithMode};
+use crate::store_path::StorePath;
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
+pub struct Output {
+    pub path: String,
+
+    #[serde(flatten)]
+    pub hash_with_mode: Option<NixHashWithMode>,
+}
+
+impl Output {
+    pub fn is_fixed(&self) -> bool {
+        self.hash_with_mode.is_some()
+    }
+
+    pub fn validate(&self, validate_output_paths: bool) -> Result<(), OutputError> {
+        if let Some(hash) = &self.hash_with_mode {
+            match hash {
+                NixHashWithMode::Flat(h) | NixHashWithMode::Recursive(h) => {
+                    if h.algo != HashAlgo::Sha1 || h.algo != HashAlgo::Sha256 {
+                        return Err(OutputError::InvalidHashAlgo(h.algo.to_string()));
+                    }
+                }
+            }
+        }
+        if validate_output_paths {
+            if let Err(e) = StorePath::from_absolute_path(&self.path) {
+                return Err(OutputError::InvalidOutputPath(self.path.to_string(), e));
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/tvix/nix-compat/src/derivation/string_escape.rs b/tvix/nix-compat/src/derivation/string_escape.rs
new file mode 100644
index 000000000000..0e1dbe516f73
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/string_escape.rs
@@ -0,0 +1,17 @@
+const STRING_ESCAPER: [(char, &str); 5] = [
+    ('\\', "\\\\"),
+    ('\n', "\\n"),
+    ('\r', "\\r"),
+    ('\t', "\\t"),
+    ('\"', "\\\""),
+];
+
+pub fn escape_string(s: &str) -> String {
+    let mut s_replaced = s.to_string();
+
+    for escape_sequence in STRING_ESCAPER {
+        s_replaced = s_replaced.replace(escape_sequence.0, escape_sequence.1);
+    }
+
+    format!("\"{}\"", s_replaced)
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv
new file mode 100644
index 000000000000..a4fea3c5f486
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv
@@ -0,0 +1 @@
+Derive([("out","/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar","r:sha256","08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba")],[],[],":",":",[],[("builder",":"),("name","bar"),("out","/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar"),("outputHash","08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),("outputHashAlgo","sha256"),("outputHashMode","recursive"),("system",":")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv.json
new file mode 100644
index 000000000000..c8bbc4cbb5be
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv.json
@@ -0,0 +1,23 @@
+{
+  "args": [],
+  "builder": ":",
+  "env": {
+    "builder": ":",
+    "name": "bar",
+    "out": "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar",
+    "outputHash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
+    "outputHashAlgo": "sha256",
+    "outputHashMode": "recursive",
+    "system": ":"
+  },
+  "inputDrvs": {},
+  "inputSrcs": [],
+  "outputs": {
+    "out": {
+      "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
+      "hashAlgo": "r:sha256",
+      "path": "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar"
+    }
+  },
+  "system": ":"
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv
new file mode 100644
index 000000000000..f0d9230a5a52
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv
@@ -0,0 +1 @@
+Derive([("out","/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json","","")],[],[],":",":",[],[("builder",":"),("json","{\"hello\":\"moto\\n\"}"),("name","nested-json"),("out","/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json"),("system",":")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv.json
new file mode 100644
index 000000000000..9cb0b43b4c09
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv.json
@@ -0,0 +1,19 @@
+{
+  "args": [],
+  "builder": ":",
+  "env": {
+    "builder": ":",
+    "json": "{\"hello\":\"moto\\n\"}",
+    "name": "nested-json",
+    "out": "/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json",
+    "system": ":"
+  },
+  "inputDrvs": {},
+  "inputSrcs": [],
+  "outputs": {
+    "out": {
+      "path": "/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json"
+    }
+  },
+  "system": ":"
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv
new file mode 100644
index 000000000000..a2cf9d31f92e
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv
@@ -0,0 +1 @@
+Derive([("out","/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo","","")],[("/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv",["out"])],[],":",":",[],[("bar","/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar"),("builder",":"),("name","foo"),("out","/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo"),("system",":")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv.json
new file mode 100644
index 000000000000..957a85ccab82
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv.json
@@ -0,0 +1,23 @@
+{
+  "args": [],
+  "builder": ":",
+  "env": {
+    "bar": "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar",
+    "builder": ":",
+    "name": "foo",
+    "out": "/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo",
+    "system": ":"
+  },
+  "inputDrvs": {
+    "/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv": [
+      "out"
+    ]
+  },
+  "inputSrcs": [],
+  "outputs": {
+    "out": {
+      "path": "/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo"
+    }
+  },
+  "system": ":"
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv
new file mode 100644
index 000000000000..bbe88c02c739
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv
@@ -0,0 +1 @@
+Derive([("out","/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode","","")],[],[],":",":",[],[("builder",":"),("letters","räksmörgås\nrødgrød med fløde\nLübeck\n肥猪\nこんにちは / 今日は\n🌮\n"),("name","unicode"),("out","/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode"),("system",":")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv.json
new file mode 100644
index 000000000000..f8f33c1bba17
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv.json
@@ -0,0 +1,19 @@
+{
+  "outputs": {
+    "out": {
+      "path": "/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode"
+    }
+  },
+  "inputSrcs": [],
+  "inputDrvs": {},
+  "system": ":",
+  "builder": ":",
+  "args": [],
+  "env": {
+    "builder": ":",
+    "letters": "räksmörgås\nrødgrød med fløde\nLübeck\n肥猪\nこんにちは / 今日は\n🌮\n",
+    "name": "unicode",
+    "out": "/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode",
+    "system": ":"
+  }
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv
new file mode 100644
index 000000000000..4b9338c0b953
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv
@@ -0,0 +1 @@
+Derive([("out","/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs","","")],[],[],":",":",[],[("__json","{\"builder\":\":\",\"name\":\"structured-attrs\",\"system\":\":\"}"),("out","/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv.json
new file mode 100644
index 000000000000..74e3d7df55c5
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv.json
@@ -0,0 +1,16 @@
+{
+  "args": [],
+  "builder": ":",
+  "env": {
+    "__json": "{\"builder\":\":\",\"name\":\"structured-attrs\",\"system\":\":\"}",
+    "out": "/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs"
+  },
+  "inputDrvs": {},
+  "inputSrcs": [],
+  "outputs": {
+    "out": {
+      "path": "/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs"
+    }
+  },
+  "system": ":"
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv
new file mode 100644
index 000000000000..1699c2a75e48
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv
@@ -0,0 +1 @@
+Derive([("out","/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo","","")],[("/nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv",["out"])],[],":",":",[],[("bar","/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"),("builder",":"),("name","foo"),("out","/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo"),("system",":")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv.json
new file mode 100644
index 000000000000..831d27956d86
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv.json
@@ -0,0 +1,23 @@
+{
+  "args": [],
+  "builder": ":",
+  "env": {
+    "bar": "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar",
+    "builder": ":",
+    "name": "foo",
+    "out": "/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo",
+    "system": ":"
+  },
+  "inputDrvs": {
+    "/nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv": [
+      "out"
+    ]
+  },
+  "inputSrcs": [],
+  "outputs": {
+    "out": {
+      "path": "/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo"
+    }
+  },
+  "system": ":"
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv
new file mode 100644
index 000000000000..523612238c76
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv
@@ -0,0 +1 @@
+Derive([("lib","/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib","",""),("out","/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out","","")],[],[],":",":",[],[("builder",":"),("lib","/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib"),("name","has-multi-out"),("out","/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out"),("outputs","out lib"),("system",":")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv.json
new file mode 100644
index 000000000000..0bd7a2991cc7
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv.json
@@ -0,0 +1,23 @@
+{
+  "args": [],
+  "builder": ":",
+  "env": {
+    "builder": ":",
+    "lib": "/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib",
+    "name": "has-multi-out",
+    "out": "/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out",
+    "outputs": "out lib",
+    "system": ":"
+  },
+  "inputDrvs": {},
+  "inputSrcs": [],
+  "outputs": {
+    "lib": {
+      "path": "/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib"
+    },
+    "out": {
+      "path": "/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out"
+    }
+  },
+  "system": ":"
+}
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv b/tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv
new file mode 100644
index 000000000000..559e93ed0ed6
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv
@@ -0,0 +1 @@
+Derive([("out","/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar","r:sha1","0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")],[],[],":",":",[],[("builder",":"),("name","bar"),("out","/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"),("outputHash","0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"),("outputHashAlgo","sha1"),("outputHashMode","recursive"),("system",":")])
\ No newline at end of file
diff --git a/tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv.json b/tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv.json
new file mode 100644
index 000000000000..e297d271592f
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/derivation_tests/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv.json
@@ -0,0 +1,23 @@
+{
+  "args": [],
+  "builder": ":",
+  "env": {
+    "builder": ":",
+    "name": "bar",
+    "out": "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar",
+    "outputHash": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
+    "outputHashAlgo": "sha1",
+    "outputHashMode": "recursive",
+    "system": ":"
+  },
+  "inputDrvs": {},
+  "inputSrcs": [],
+  "outputs": {
+    "out": {
+      "hash": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
+      "hashAlgo": "r:sha1",
+      "path": "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"
+    }
+  },
+  "system": ":"
+}
diff --git a/tvix/nix-compat/src/derivation/tests/mod.rs b/tvix/nix-compat/src/derivation/tests/mod.rs
new file mode 100644
index 000000000000..5daa16da03cb
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/tests/mod.rs
@@ -0,0 +1,315 @@
+use crate::derivation::output::Output;
+use crate::derivation::Derivation;
+use crate::nixhash::NixHash;
+use crate::store_path::StorePath;
+use std::collections::BTreeSet;
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+use test_case::test_case;
+use test_generator::test_resources;
+
+const RESOURCES_PATHS: &str = "src/derivation/tests/derivation_tests";
+
+fn read_file(path: &str) -> String {
+    let path = Path::new(path);
+    let mut file = File::open(path).unwrap();
+    let mut data = String::new();
+
+    file.read_to_string(&mut data).unwrap();
+
+    data
+}
+
+#[test_resources("src/derivation/tests/derivation_tests/*.drv")]
+fn check_serizaliation(path_to_drv_file: &str) {
+    let data = read_file(&format!("{}.json", path_to_drv_file));
+    let derivation: Derivation = serde_json::from_str(&data).expect("JSON was not well-formatted");
+
+    let mut serialized_derivation = String::new();
+    derivation.serialize(&mut serialized_derivation).unwrap();
+
+    let expected = read_file(path_to_drv_file);
+
+    assert_eq!(expected, serialized_derivation);
+}
+
+#[test_resources("src/derivation/tests/derivation_tests/*.drv")]
+fn validate(path_to_drv_file: &str) {
+    let data = read_file(&format!("{}.json", path_to_drv_file));
+    let derivation: Derivation = serde_json::from_str(&data).expect("JSON was not well-formatted");
+
+    derivation
+        .validate(true)
+        .expect("derivation failed to validate")
+}
+
+#[test_resources("src/derivation/tests/derivation_tests/*.drv")]
+fn check_to_aterm_string(path_to_drv_file: &str) {
+    let data = read_file(&format!("{}.json", path_to_drv_file));
+    let derivation: Derivation = serde_json::from_str(&data).expect("JSON was not well-formatted");
+
+    let expected = read_file(path_to_drv_file);
+
+    assert_eq!(expected, derivation.to_aterm_string());
+}
+
+#[test_case("bar","0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv"; "fixed_sha256")]
+#[test_case("foo", "4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv"; "simple-sha256")]
+#[test_case("bar", "ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv"; "fixed-sha1")]
+#[test_case("foo", "ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv"; "simple-sha1")]
+#[test_case("has-multi-out", "h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv"; "multiple-outputs")]
+#[test_case("structured-attrs", "9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv"; "structured-attrs")]
+#[test_case("unicode", "52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv"; "unicode")]
+fn derivation_path(name: &str, expected_path: &str) {
+    let data = read_file(&format!("{}/{}.json", RESOURCES_PATHS, expected_path));
+    let derivation: Derivation = serde_json::from_str(&data).expect("JSON was not well-formatted");
+
+    assert_eq!(
+        derivation.calculate_derivation_path(name).unwrap(),
+        StorePath::from_string(expected_path).unwrap()
+    );
+}
+
+/// This trims all output paths from a Derivation struct,
+/// by setting outputs[$outputName].path and environment[$outputName] to the empty string.
+fn derivation_with_trimmed_output_paths(derivation: &Derivation) -> Derivation {
+    let mut trimmed_env = derivation.environment.clone();
+    let mut trimmed_outputs = derivation.outputs.clone();
+
+    for (output_name, output) in &derivation.outputs {
+        trimmed_env.insert(output_name.clone(), "".to_string());
+        assert!(trimmed_outputs.contains_key(output_name));
+        trimmed_outputs.insert(
+            output_name.to_string(),
+            Output {
+                path: "".to_string(),
+                ..output.clone()
+            },
+        );
+    }
+
+    // replace environment and outputs with the trimmed variants
+    Derivation {
+        environment: trimmed_env,
+        outputs: trimmed_outputs,
+        ..derivation.clone()
+    }
+}
+
+#[test_case("0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv", "sha256:724f3e3634fce4cbbbd3483287b8798588e80280660b9a63fd13a1bc90485b33"; "fixed_sha256")]
+#[test_case("ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv", "sha256:c79aebd0ce3269393d4a1fde2cbd1d975d879b40f0bf40a48f550edc107fd5df";"fixed-sha1")]
+fn derivation_or_fod_hash(drv_path: &str, expected_nix_hash_string: &str) {
+    // read in the fixture
+    let data = read_file(&format!("{}/{}.json", RESOURCES_PATHS, drv_path));
+    let drv: Derivation = serde_json::from_str(&data).expect("must deserialize");
+
+    let actual = drv.derivation_or_fod_hash(|_| panic!("must not be called"));
+
+    assert_eq!(expected_nix_hash_string, actual.to_nix_hash_string());
+}
+
+#[test_case("bar","0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv"; "fixed_sha256")]
+#[test_case("foo", "4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv"; "simple-sha256")]
+#[test_case("bar", "ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv"; "fixed-sha1")]
+#[test_case("foo", "ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv"; "simple-sha1")]
+#[test_case("has-multi-out", "h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv"; "multiple-outputs")]
+#[test_case("structured-attrs", "9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv"; "structured-attrs")]
+#[test_case("unicode", "52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv"; "unicode")]
+fn output_paths(name: &str, drv_path: &str) {
+    // read in the fixture
+    let data = read_file(&format!("{}/{}.json", RESOURCES_PATHS, drv_path));
+    let expected_derivation: Derivation = serde_json::from_str(&data).expect("must deserialize");
+
+    let mut derivation = derivation_with_trimmed_output_paths(&expected_derivation);
+
+    // calculate the derivation_or_fod_hash of derivation
+    // We don't expect the lookup function to be called for most derivations.
+    let calculated_derivation_or_fod_hash = derivation.derivation_or_fod_hash(|parent_drv_path| {
+        // 4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv may lookup /nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv
+        // ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv may lookup /nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv
+        if name == "foo"
+            && ((drv_path == "4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv"
+                && parent_drv_path == "/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv")
+                || (drv_path == "ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv"
+                    && parent_drv_path == "/nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv"))
+        {
+            // do the lookup, by reading in the fixture of the requested
+            // drv_name, and calculating its drv replacement (on the non-stripped version)
+            // In a real-world scenario you would have already done this during construction.
+
+            let data = read_file(&format!(
+                "{}/{}.json",
+                RESOURCES_PATHS,
+                Path::new(parent_drv_path)
+                    .file_name()
+                    .unwrap()
+                    .to_string_lossy()
+            ));
+
+            let drv: Derivation = serde_json::from_str(&data).expect("must deserialize");
+
+            // calculate derivation_or_fod_hash for each parent.
+            // This may not trigger subsequent requests, as both parents are FOD.
+            drv.derivation_or_fod_hash(|_| panic!("must not lookup"))
+        } else {
+            // we only expect this to be called in the "foo" testcase, for the "bar derivations"
+            panic!("may only be called for foo testcase on bar derivations");
+        }
+    });
+
+    derivation
+        .calculate_output_paths(name, &calculated_derivation_or_fod_hash)
+        .unwrap();
+
+    // The derivation should now look like it was before
+    assert_eq!(expected_derivation, derivation);
+}
+
+/// Exercises the output path calculation functions like a constructing client
+/// (an implementation of builtins.derivation) would do:
+///
+/// ```nix
+/// rec {
+///   bar = builtins.derivation {
+///     name = "bar";
+///     builder = ":";
+///     system = ":";
+///     outputHash = "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba";
+///     outputHashAlgo = "sha256";
+///     outputHashMode = "recursive";
+///   };
+///
+///   foo = builtins.derivation {
+///     name = "foo";
+///     builder = ":";
+///     system = ":";
+///     inherit bar;
+///   };
+/// }
+/// ```
+/// It first assembles the bar derivation, does the output path calculation on
+/// it, then continues with the foo derivation.
+///
+/// The code ensures the resulting Derivations match our fixtures.
+#[test]
+fn output_path_construction() {
+    // create the bar derivation
+    let mut bar_drv = Derivation {
+        builder: ":".to_string(),
+        system: ":".to_string(),
+        ..Default::default()
+    };
+
+    // assemble bar env
+    let bar_env = &mut bar_drv.environment;
+    bar_env.insert("builder".to_string(), ":".to_string());
+    bar_env.insert("name".to_string(), "bar".to_string());
+    bar_env.insert("out".to_string(), "".to_string()); // will be calculated
+    bar_env.insert(
+        "outputHash".to_string(),
+        "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba".to_string(),
+    );
+    bar_env.insert("outputHashAlgo".to_string(), "sha256".to_string());
+    bar_env.insert("outputHashMode".to_string(), "recursive".to_string());
+    bar_env.insert("system".to_string(), ":".to_string());
+
+    // assemble bar outputs
+    bar_drv.outputs.insert(
+        "out".to_string(),
+        Output {
+            path: "".to_string(), // will be calculated
+            hash_with_mode: Some(crate::nixhash::NixHashWithMode::Recursive(NixHash {
+                digest: data_encoding::HEXLOWER
+                    .decode(
+                        "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"
+                            .as_bytes(),
+                    )
+                    .unwrap(),
+                algo: crate::nixhash::HashAlgo::Sha256,
+            })),
+        },
+    );
+
+    // calculate bar output paths
+    let bar_calc_result = bar_drv.calculate_output_paths(
+        "bar",
+        &bar_drv.derivation_or_fod_hash(|_| panic!("is FOD, should not lookup")),
+    );
+    assert!(bar_calc_result.is_ok());
+
+    // ensure it matches our bar fixture
+    let bar_data = read_file(&format!(
+        "{}/{}.json",
+        RESOURCES_PATHS, "0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv"
+    ));
+    let bar_drv_expected: Derivation = serde_json::from_str(&bar_data).expect("must deserialize");
+    assert_eq!(bar_drv_expected, bar_drv);
+
+    // now construct foo, which requires bar_drv
+    // Note how we refer to the output path, drv name and replacement_str (with calculated output paths) of bar.
+    let bar_output_path = &bar_drv.outputs.get("out").expect("must exist").path;
+    let bar_drv_derivation_or_fod_hash =
+        bar_drv.derivation_or_fod_hash(|_| panic!("is FOD, should not lookup"));
+
+    let bar_drv_path = bar_drv
+        .calculate_derivation_path("bar")
+        .expect("must succeed");
+
+    // create foo derivation
+    let mut foo_drv = Derivation {
+        builder: ":".to_string(),
+        system: ":".to_string(),
+        ..Default::default()
+    };
+
+    // assemble foo env
+    let foo_env = &mut foo_drv.environment;
+    foo_env.insert("bar".to_string(), bar_output_path.to_string());
+    foo_env.insert("builder".to_string(), ":".to_string());
+    foo_env.insert("name".to_string(), "foo".to_string());
+    foo_env.insert("out".to_string(), "".to_string()); // will be calculated
+    foo_env.insert("system".to_string(), ":".to_string());
+
+    // asssemble foo outputs
+    foo_drv.outputs.insert(
+        "out".to_string(),
+        Output {
+            path: "".to_string(), // will be calculated
+            hash_with_mode: None,
+        },
+    );
+
+    // assemble foo input_derivations
+    foo_drv.input_derivations.insert(
+        bar_drv_path.to_absolute_path(),
+        BTreeSet::from(["out".to_string()]),
+    );
+
+    // calculate foo output paths
+    let foo_calc_result = foo_drv.calculate_output_paths(
+        "foo",
+        &foo_drv.derivation_or_fod_hash(|drv_path| {
+            if drv_path != "/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv" {
+                panic!("lookup called with unexpected drv_path: {}", drv_path);
+            }
+            bar_drv_derivation_or_fod_hash.clone()
+        }),
+    );
+    assert!(foo_calc_result.is_ok());
+
+    // ensure it matches our foo fixture
+    let foo_data = read_file(&format!(
+        "{}/{}.json",
+        RESOURCES_PATHS, "4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv",
+    ));
+    let foo_drv_expected: Derivation = serde_json::from_str(&foo_data).expect("must deserialize");
+    assert_eq!(foo_drv_expected, foo_drv);
+
+    assert_eq!(
+        StorePath::from_string("4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv").expect("must succeed"),
+        foo_drv
+            .calculate_derivation_path("foo")
+            .expect("must succeed")
+    );
+}
diff --git a/tvix/nix-compat/src/derivation/validate.rs b/tvix/nix-compat/src/derivation/validate.rs
new file mode 100644
index 000000000000..d8dc24a92ae1
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/validate.rs
@@ -0,0 +1,129 @@
+use crate::derivation::{Derivation, DerivationError};
+use crate::store_path::StorePath;
+
+impl Derivation {
+    /// validate ensures a Derivation struct is properly populated,
+    /// and returns a [DerivationError] if not.
+    ///
+    /// if `validate_output_paths` is set to false, the output paths are
+    /// excluded from validation.
+    ///
+    /// This is helpful to validate struct population before invoking
+    /// [Derivation::calculate_output_paths].
+    pub fn validate(&self, validate_output_paths: bool) -> Result<(), DerivationError> {
+        // Ensure the number of outputs is > 1
+        if self.outputs.is_empty() {
+            return Err(DerivationError::NoOutputs());
+        }
+
+        // Validate all outputs
+        for (output_name, output) in &self.outputs {
+            // empty output names are invalid.
+            //
+            // `drv` is an invalid output name too, as this would cause
+            // a `builtins.derivation` call to return an attrset with a
+            // `drvPath` key (which already exists) and has a different
+            // meaning.
+            //
+            // Other output names that don't match the name restrictions from
+            // [StorePath] will fail the [StorePath::validate_name] check.
+            if output_name.is_empty()
+                || output_name == "drv"
+                || StorePath::validate_name(output_name).is_err()
+            {
+                return Err(DerivationError::InvalidOutputName(output_name.to_string()));
+            }
+
+            if output.is_fixed() {
+                if self.outputs.len() != 1 {
+                    return Err(DerivationError::MoreThanOneOutputButFixed());
+                }
+                if output_name != "out" {
+                    return Err(DerivationError::InvalidOutputNameForFixed(
+                        output_name.to_string(),
+                    ));
+                }
+
+                break;
+            }
+
+            if let Err(e) = output.validate(validate_output_paths) {
+                return Err(DerivationError::InvalidOutput(output_name.to_string(), e));
+            }
+        }
+
+        // Validate all input_derivations
+        for (input_derivation_path, output_names) in &self.input_derivations {
+            // Validate input_derivation_path
+            if let Err(e) = StorePath::from_absolute_path(input_derivation_path) {
+                return Err(DerivationError::InvalidInputDerivationPath(
+                    input_derivation_path.to_string(),
+                    e,
+                ));
+            }
+
+            if !input_derivation_path.ends_with(".drv") {
+                return Err(DerivationError::InvalidInputDerivationPrefix(
+                    input_derivation_path.to_string(),
+                ));
+            }
+
+            if output_names.is_empty() {
+                return Err(DerivationError::EmptyInputDerivationOutputNames(
+                    input_derivation_path.to_string(),
+                ));
+            }
+
+            for output_name in output_names.iter() {
+                // empty output names are invalid.
+                //
+                // `drv` is an invalid output name too, as this would cause
+                // a `builtins.derivation` call to return an attrset with a
+                // `drvPath` key (which already exists) and has a different
+                // meaning.
+                //
+                // Other output names that don't match the name restrictions from
+                // [StorePath] will fail the [StorePath::validate_name] check.
+                if output_name.is_empty()
+                    || output_name == "drv"
+                    || StorePath::validate_name(output_name).is_err()
+                {
+                    return Err(DerivationError::InvalidInputDerivationOutputName(
+                        input_derivation_path.to_string(),
+                        output_name.to_string(),
+                    ));
+                }
+            }
+        }
+
+        // Validate all input_sources
+        for input_source in self.input_sources.iter() {
+            if let Err(e) = StorePath::from_absolute_path(input_source) {
+                return Err(DerivationError::InvalidInputSourcesPath(
+                    input_source.to_string(),
+                    e,
+                ));
+            }
+        }
+
+        // validate platform
+        if self.system.is_empty() {
+            return Err(DerivationError::InvalidPlatform(self.system.to_string()));
+        }
+
+        // validate builder
+        if self.builder.is_empty() {
+            return Err(DerivationError::InvalidBuilder(self.builder.to_string()));
+        }
+
+        // validate env, none of the keys may be empty.
+        // We skip the `name` validation seen in go-nix.
+        for k in self.environment.keys() {
+            if k.is_empty() {
+                return Err(DerivationError::InvalidEnvironmentKey(k.to_string()));
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/tvix/nix-compat/src/derivation/write.rs b/tvix/nix-compat/src/derivation/write.rs
new file mode 100644
index 000000000000..52166294e078
--- /dev/null
+++ b/tvix/nix-compat/src/derivation/write.rs
@@ -0,0 +1,190 @@
+//! This module implements the serialisation of derivations into the
+//! [ATerm][] format used by C++ Nix.
+//!
+//! [ATerm]: http://program-transformation.org/Tools/ATermFormat.html
+
+use crate::derivation::output::Output;
+use crate::derivation::string_escape::escape_string;
+use std::collections::BTreeSet;
+use std::{collections::BTreeMap, fmt, fmt::Write};
+
+pub const DERIVATION_PREFIX: &str = "Derive";
+pub const PAREN_OPEN: char = '(';
+pub const PAREN_CLOSE: char = ')';
+pub const BRACKET_OPEN: char = '[';
+pub const BRACKET_CLOSE: char = ']';
+pub const COMMA: char = ',';
+pub const QUOTE: char = '"';
+
+fn write_array_elements(
+    writer: &mut impl Write,
+    quote: bool,
+    open: &str,
+    closing: &str,
+    elements: Vec<&str>,
+) -> Result<(), fmt::Error> {
+    writer.write_str(open)?;
+
+    for (index, element) in elements.iter().enumerate() {
+        if index > 0 {
+            writer.write_char(COMMA)?;
+        }
+
+        if quote {
+            writer.write_char(QUOTE)?;
+        }
+
+        writer.write_str(element)?;
+
+        if quote {
+            writer.write_char(QUOTE)?;
+        }
+    }
+
+    writer.write_str(closing)?;
+
+    Ok(())
+}
+
+pub fn write_outputs(
+    writer: &mut impl Write,
+    outputs: &BTreeMap<String, Output>,
+) -> Result<(), fmt::Error> {
+    writer.write_char(BRACKET_OPEN)?;
+    for (ii, (output_name, output)) in outputs.iter().enumerate() {
+        if ii > 0 {
+            writer.write_char(COMMA)?;
+        }
+
+        let mut elements: Vec<&str> = vec![output_name, &output.path];
+
+        let (e2, e3) = match &output.hash_with_mode {
+            Some(hash) => match hash {
+                crate::nixhash::NixHashWithMode::Flat(h) => (
+                    h.algo.to_string(),
+                    data_encoding::HEXLOWER.encode(&h.digest),
+                ),
+                crate::nixhash::NixHashWithMode::Recursive(h) => (
+                    format!("r:{}", h.algo),
+                    data_encoding::HEXLOWER.encode(&h.digest),
+                ),
+            },
+            None => ("".to_string(), "".to_string()),
+        };
+
+        elements.push(&e2);
+        elements.push(&e3);
+
+        write_array_elements(
+            writer,
+            true,
+            &PAREN_OPEN.to_string(),
+            &PAREN_CLOSE.to_string(),
+            elements,
+        )?
+    }
+    writer.write_char(BRACKET_CLOSE)?;
+
+    Ok(())
+}
+
+pub fn write_input_derivations(
+    writer: &mut impl Write,
+    input_derivations: &BTreeMap<String, BTreeSet<String>>,
+) -> Result<(), fmt::Error> {
+    writer.write_char(COMMA)?;
+    writer.write_char(BRACKET_OPEN)?;
+
+    for (ii, (input_derivation_path, input_derivation)) in input_derivations.iter().enumerate() {
+        if ii > 0 {
+            writer.write_char(COMMA)?;
+        }
+
+        writer.write_char(PAREN_OPEN)?;
+        writer.write_char(QUOTE)?;
+        writer.write_str(input_derivation_path.as_str())?;
+        writer.write_char(QUOTE)?;
+        writer.write_char(COMMA)?;
+
+        write_array_elements(
+            writer,
+            true,
+            &BRACKET_OPEN.to_string(),
+            &BRACKET_CLOSE.to_string(),
+            input_derivation.iter().map(|s| &**s).collect(),
+        )?;
+
+        writer.write_char(PAREN_CLOSE)?;
+    }
+
+    writer.write_char(BRACKET_CLOSE)?;
+
+    Ok(())
+}
+
+pub fn write_input_sources(
+    writer: &mut impl Write,
+    input_sources: &BTreeSet<String>,
+) -> Result<(), fmt::Error> {
+    writer.write_char(COMMA)?;
+
+    write_array_elements(
+        writer,
+        true,
+        &BRACKET_OPEN.to_string(),
+        &BRACKET_CLOSE.to_string(),
+        input_sources.iter().map(|s| &**s).collect(),
+    )?;
+
+    Ok(())
+}
+
+pub fn write_system(writer: &mut impl Write, platform: &str) -> Result<(), fmt::Error> {
+    writer.write_char(COMMA)?;
+    writer.write_str(escape_string(platform).as_str())?;
+    Ok(())
+}
+
+pub fn write_builder(writer: &mut impl Write, builder: &str) -> Result<(), fmt::Error> {
+    writer.write_char(COMMA)?;
+    writer.write_str(escape_string(builder).as_str())?;
+    Ok(())
+}
+pub fn write_arguments(writer: &mut impl Write, arguments: &[String]) -> Result<(), fmt::Error> {
+    writer.write_char(COMMA)?;
+    write_array_elements(
+        writer,
+        true,
+        &BRACKET_OPEN.to_string(),
+        &BRACKET_CLOSE.to_string(),
+        arguments.iter().map(|s| &**s).collect(),
+    )?;
+
+    Ok(())
+}
+
+pub fn write_enviroment(
+    writer: &mut impl Write,
+    environment: &BTreeMap<String, String>,
+) -> Result<(), fmt::Error> {
+    writer.write_char(COMMA)?;
+    writer.write_char(BRACKET_OPEN)?;
+
+    for (ii, (key, environment)) in environment.iter().enumerate() {
+        if ii > 0 {
+            writer.write_char(COMMA)?;
+        }
+
+        write_array_elements(
+            writer,
+            false,
+            &PAREN_OPEN.to_string(),
+            &PAREN_CLOSE.to_string(),
+            vec![&escape_string(key), &escape_string(environment)],
+        )?;
+    }
+
+    writer.write_char(BRACKET_CLOSE)?;
+
+    Ok(())
+}
diff --git a/tvix/nix-compat/src/lib.rs b/tvix/nix-compat/src/lib.rs
new file mode 100644
index 000000000000..37d79f38fb37
--- /dev/null
+++ b/tvix/nix-compat/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod derivation;
+pub mod nar;
+pub mod nixbase32;
+pub mod nixhash;
+pub mod store_path;
diff --git a/tvix/nix-compat/src/nar/mod.rs b/tvix/nix-compat/src/nar/mod.rs
new file mode 100644
index 000000000000..d3baa817825a
--- /dev/null
+++ b/tvix/nix-compat/src/nar/mod.rs
@@ -0,0 +1 @@
+pub mod writer;
diff --git a/tvix/nix-compat/src/nar/writer/mod.rs b/tvix/nix-compat/src/nar/writer/mod.rs
new file mode 100644
index 000000000000..f24b69883876
--- /dev/null
+++ b/tvix/nix-compat/src/nar/writer/mod.rs
@@ -0,0 +1,219 @@
+//! Implements an interface for writing the Nix archive format (NAR).
+//!
+//! NAR files (and their hashed representations) are used in C++ Nix for
+//! addressing fixed-output derivations and a variety of other things.
+//!
+//! NAR files can be output to any type that implements [`Write`], and content
+//! can be read from any type that implementes [`BufRead`].
+//!
+//! Writing a single file might look like this:
+//!
+//! ```rust
+//! # use std::io::BufReader;
+//! # let some_file: Vec<u8> = vec![0, 1, 2, 3, 4];
+//!
+//! // Output location to write the NAR to.
+//! let mut sink: Vec<u8> = Vec::new();
+//!
+//! // Instantiate writer for this output location.
+//! let mut nar = nix_compat::nar::writer::open(&mut sink)?;
+//!
+//! // Acquire metadata for the single file to output, and pass it in a
+//! // `BufRead`-implementing type.
+//!
+//! let executable = false;
+//! let size = some_file.len() as u64;
+//! let mut reader = BufReader::new(some_file.as_slice());
+//! nar.file(executable, size, &mut reader)?;
+//! # Ok::<(), std::io::Error>(())
+//! ```
+
+use std::io::{
+    self, BufRead,
+    ErrorKind::{InvalidInput, UnexpectedEof},
+    Write,
+};
+
+mod wire;
+
+/// Convenience type alias for types implementing [`Write`].
+pub type Writer<'a> = dyn Write + 'a;
+
+/// Create a new NAR, writing the output to the specified writer.
+pub fn open<'a, 'w: 'a>(writer: &'a mut Writer<'w>) -> io::Result<Node<'a, 'w>> {
+    let mut node = Node { writer };
+    node.write(&wire::TOK_NAR)?;
+    Ok(node)
+}
+
+/// Single node in a NAR file.
+///
+/// A NAR can be thought of as a tree of nodes represented by this type. Each
+/// node can be a file, a symlink or a directory containing other nodes.
+pub struct Node<'a, 'w: 'a> {
+    writer: &'a mut Writer<'w>,
+}
+
+impl<'a, 'w> Node<'a, 'w> {
+    fn write(&mut self, data: &[u8]) -> io::Result<()> {
+        self.writer.write_all(data)
+    }
+
+    fn pad(&mut self, n: u64) -> io::Result<()> {
+        match (n & 7) as usize {
+            0 => Ok(()),
+            n => self.write(&[0; 8][n..]),
+        }
+    }
+
+    /// Make this node a symlink.
+    pub fn symlink(mut self, target: &str) -> io::Result<()> {
+        debug_assert!(
+            target.len() <= wire::MAX_TARGET_LEN,
+            "target.len() > {}",
+            wire::MAX_TARGET_LEN
+        );
+        debug_assert!(
+            !target.contains('\0'),
+            "invalid target characters: {target:?}"
+        );
+        debug_assert!(!target.is_empty(), "empty target");
+
+        self.write(&wire::TOK_SYM)?;
+        self.write(&target.len().to_le_bytes())?;
+        self.write(target.as_bytes())?;
+        self.pad(target.len() as u64)?;
+        self.write(&wire::TOK_PAR)?;
+        Ok(())
+    }
+
+    /// Make this node a single file.
+    pub fn file(mut self, executable: bool, size: u64, reader: &mut dyn BufRead) -> io::Result<()> {
+        self.write(if executable {
+            &wire::TOK_EXE
+        } else {
+            &wire::TOK_REG
+        })?;
+
+        self.write(&size.to_le_bytes())?;
+
+        let mut need = size;
+        while need != 0 {
+            let data = reader.fill_buf()?;
+
+            if data.is_empty() {
+                return Err(UnexpectedEof.into());
+            }
+
+            let n = need.min(data.len() as u64) as usize;
+            self.write(&data[..n])?;
+
+            need -= n as u64;
+            reader.consume(n);
+        }
+
+        // bail if there's still data left in the passed reader.
+        // This uses the same code as [BufRead::has_data_left] (unstable).
+        if reader.fill_buf().map(|b| !b.is_empty())? {
+            return Err(io::Error::new(
+                InvalidInput,
+                "reader contained more data than specified size",
+            ));
+        }
+
+        self.pad(size)?;
+        self.write(&wire::TOK_PAR)?;
+
+        Ok(())
+    }
+
+    /// Make this node a directory, the content of which is set using the
+    /// resulting [`Directory`] value.
+    pub fn directory(mut self) -> io::Result<Directory<'a, 'w>> {
+        self.write(&wire::TOK_DIR)?;
+        Ok(Directory::new(self))
+    }
+}
+
+#[cfg(debug_assertions)]
+type Name = String;
+#[cfg(not(debug_assertions))]
+type Name = ();
+
+fn into_name(_name: &str) -> Name {
+    #[cfg(debug_assertions)]
+    _name.to_owned()
+}
+
+/// Content of a NAR node that represents a directory.
+pub struct Directory<'a, 'w> {
+    node: Node<'a, 'w>,
+    prev_name: Option<Name>,
+}
+
+impl<'a, 'w> Directory<'a, 'w> {
+    fn new(node: Node<'a, 'w>) -> Self {
+        Self {
+            node,
+            prev_name: None,
+        }
+    }
+
+    /// Add an entry to the directory.
+    ///
+    /// The entry is simply another [`Node`], which can then be filled like the
+    /// root of a NAR (including, of course, by nesting directories).
+    pub fn entry(&mut self, name: &str) -> io::Result<Node<'_, 'w>> {
+        debug_assert!(
+            name.len() <= wire::MAX_NAME_LEN,
+            "name.len() > {}",
+            wire::MAX_NAME_LEN
+        );
+        debug_assert!(!["", ".", ".."].contains(&name), "invalid name: {name:?}");
+        debug_assert!(
+            !name.contains(['/', '\0']),
+            "invalid name characters: {name:?}"
+        );
+
+        match self.prev_name {
+            None => {
+                self.prev_name = Some(into_name(name));
+            }
+            Some(ref mut _prev_name) => {
+                #[cfg(debug_assertions)]
+                {
+                    assert!(
+                        &**_prev_name < name,
+                        "misordered names: {_prev_name:?} >= {name:?}"
+                    );
+                    _prev_name.clear();
+                    _prev_name.push_str(name);
+                }
+                self.node.write(&wire::TOK_PAR)?;
+            }
+        }
+
+        self.node.write(&wire::TOK_ENT)?;
+        self.node.write(&name.len().to_le_bytes())?;
+        self.node.write(name.as_bytes())?;
+        self.node.pad(name.len() as u64)?;
+        self.node.write(&wire::TOK_NOD)?;
+
+        Ok(Node {
+            writer: &mut *self.node.writer,
+        })
+    }
+
+    /// Close a directory and write terminators for the directory to the NAR.
+    ///
+    /// **Important:** This *must* be called when all entries have been written
+    /// in a directory, otherwise the resulting NAR file will be invalid.
+    pub fn close(mut self) -> io::Result<()> {
+        if self.prev_name.is_some() {
+            self.node.write(&wire::TOK_PAR)?;
+        }
+
+        self.node.write(&wire::TOK_PAR)?;
+        Ok(())
+    }
+}
diff --git a/tvix/nix-compat/src/nar/writer/wire.rs b/tvix/nix-compat/src/nar/writer/wire.rs
new file mode 100644
index 000000000000..98581ae3aa7c
--- /dev/null
+++ b/tvix/nix-compat/src/nar/writer/wire.rs
@@ -0,0 +1,46 @@
+pub const MAX_NAME_LEN: usize = 255;
+pub const MAX_TARGET_LEN: usize = 4095;
+
+#[cfg(test)]
+fn token(xs: &[&str]) -> Vec<u8> {
+    let mut out = vec![];
+    for x in xs {
+        let len = x.len() as u64;
+        out.extend_from_slice(&len.to_le_bytes());
+        out.extend_from_slice(x.as_bytes());
+
+        let n = x.len() & 7;
+        if n != 0 {
+            const ZERO: [u8; 8] = [0; 8];
+            out.extend_from_slice(&ZERO[n..]);
+        }
+    }
+    out
+}
+
+pub const TOK_NAR: [u8; 56] = *b"\x0d\0\0\0\0\0\0\0nix-archive-1\0\0\0\x01\0\0\0\0\0\0\0(\0\0\0\0\0\0\0\x04\0\0\0\0\0\0\0type\0\0\0\0";
+pub const TOK_REG: [u8; 32] = *b"\x07\0\0\0\0\0\0\0regular\0\x08\0\0\0\0\0\0\0contents";
+pub const TOK_EXE: [u8; 64] = *b"\x07\0\0\0\0\0\0\0regular\0\x0a\0\0\0\0\0\0\0executable\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x08\0\0\0\0\0\0\0contents";
+pub const TOK_SYM: [u8; 32] = *b"\x07\0\0\0\0\0\0\0symlink\0\x06\0\0\0\0\0\0\0target\0\0";
+pub const TOK_DIR: [u8; 24] = *b"\x09\0\0\0\0\0\0\0directory\0\0\0\0\0\0\0";
+pub const TOK_ENT: [u8; 48] = *b"\x05\0\0\0\0\0\0\0entry\0\0\0\x01\0\0\0\0\0\0\0(\0\0\0\0\0\0\0\x04\0\0\0\0\0\0\0name\0\0\0\0";
+pub const TOK_NOD: [u8; 48] = *b"\x04\0\0\0\0\0\0\0node\0\0\0\0\x01\0\0\0\0\0\0\0(\0\0\0\0\0\0\0\x04\0\0\0\0\0\0\0type\0\0\0\0";
+pub const TOK_PAR: [u8; 16] = *b"\x01\0\0\0\0\0\0\0)\0\0\0\0\0\0\0";
+
+#[test]
+fn tokens() {
+    let cases: &[(&[u8], &[&str])] = &[
+        (&TOK_NAR, &["nix-archive-1", "(", "type"]),
+        (&TOK_REG, &["regular", "contents"]),
+        (&TOK_EXE, &["regular", "executable", "", "contents"]),
+        (&TOK_SYM, &["symlink", "target"]),
+        (&TOK_DIR, &["directory"]),
+        (&TOK_ENT, &["entry", "(", "name"]),
+        (&TOK_NOD, &["node", "(", "type"]),
+        (&TOK_PAR, &[")"]),
+    ];
+
+    for &(tok, xs) in cases {
+        assert_eq!(tok, token(xs));
+    }
+}
diff --git a/tvix/nix-compat/src/nixbase32.rs b/tvix/nix-compat/src/nixbase32.rs
new file mode 100644
index 000000000000..3d1c276c63bb
--- /dev/null
+++ b/tvix/nix-compat/src/nixbase32.rs
@@ -0,0 +1,167 @@
+//! Implements the slightly odd "base32" encoding that's used in Nix.
+//!
+//! Nix uses a custom alphabet. Contrary to other implementations (RFC4648),
+//! encoding to "nix base32" doesn't use any padding, and reads in characters
+//! in reverse order.
+//!
+//! This is also the main reason why we can't use `data_encoding::Encoding` -
+//! it gets things wrong if there normally would be a need for padding.
+
+use std::fmt::Write;
+
+use thiserror::Error;
+
+const ALPHABET: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz";
+
+/// Errors that can occur while decoding nixbase32-encoded data.
+#[derive(Debug, Eq, PartialEq, Error)]
+pub enum Nixbase32DecodeError {
+    #[error("character {0:x} not in alphabet")]
+    CharacterNotInAlphabet(u8),
+    #[error("nonzero carry")]
+    NonzeroCarry(),
+}
+
+/// Returns encoded input
+pub fn encode(input: &[u8]) -> String {
+    let output_len = encode_len(input.len());
+    let mut output = String::with_capacity(output_len);
+
+    if output_len > 0 {
+        for n in (0..=output_len - 1).rev() {
+            let b = n * 5; // bit offset within the entire input
+            let i = b / 8; // input byte index
+            let j = b % 8; // bit offset within that input byte
+
+            let mut c = input[i] >> j;
+            if i + 1 < input.len() {
+                // we want to right shift, and discard shifted out bits (unchecked)
+                // To do this without panicing, we need to do the shifting in u16
+                // and convert back to u8 afterwards.
+                c |= ((input[i + 1] as u16) << (8 - j as u16)) as u8
+            }
+
+            output
+                .write_char(ALPHABET[(c & 0x1f) as usize] as char)
+                .unwrap();
+        }
+    }
+
+    output
+}
+
+/// This maps a nixbase32-encoded character to its binary representation, which
+/// is also the index of the character in the alphabet.
+fn decode_char(encoded_char: &u8) -> Option<u8> {
+    Some(match encoded_char {
+        b'0'..=b'9' => encoded_char - b'0',
+        b'a'..=b'd' => encoded_char - b'a' + 10_u8,
+        b'f'..=b'n' => encoded_char - b'f' + 14_u8,
+        b'p'..=b's' => encoded_char - b'p' + 23_u8,
+        b'v'..=b'z' => encoded_char - b'v' + 27_u8,
+        _ => return None,
+    })
+}
+
+/// Returns decoded input
+pub fn decode(input: &[u8]) -> Result<Vec<u8>, Nixbase32DecodeError> {
+    let output_len = decode_len(input.len());
+    let mut output: Vec<u8> = vec![0x00; output_len];
+
+    // loop over all characters in reverse, and keep the iteration count in n.
+    for (n, c) in input.iter().rev().enumerate() {
+        match decode_char(c) {
+            None => return Err(Nixbase32DecodeError::CharacterNotInAlphabet(*c)),
+            Some(c_decoded) => {
+                let b = n * 5;
+                let i = b / 8;
+                let j = b % 8;
+
+                let val = (c_decoded as u16).rotate_left(j as u32);
+                output[i] |= (val & 0x00ff) as u8;
+                let carry = ((val & 0xff00) >> 8) as u8;
+
+                // if we're at the end of dst…
+                if i == output_len - 1 {
+                    // but have a nonzero carry, the encoding is invalid.
+                    if carry != 0 {
+                        return Err(Nixbase32DecodeError::NonzeroCarry());
+                    }
+                } else {
+                    output[i + 1] |= carry;
+                }
+            }
+        }
+    }
+
+    Ok(output)
+}
+
+/// Returns the decoded length of an input of length len.
+pub fn decode_len(len: usize) -> usize {
+    (len * 5) / 8
+}
+
+/// Returns the encoded length of an input of length len
+pub fn encode_len(len: usize) -> usize {
+    if len == 0 {
+        return 0;
+    }
+    (len * 8 - 1) / 5 + 1
+}
+
+#[cfg(test)]
+mod tests {
+    use test_case::test_case;
+
+    #[test_case("", vec![] ; "empty bytes")]
+    #[test_case("0z", vec![0x1f]; "one byte")]
+    #[test_case("00bgd045z0d4icpbc2yyz4gx48ak44la", vec![
+                 0x8a, 0x12, 0x32, 0x15, 0x22, 0xfd, 0x91, 0xef, 0xbd, 0x60, 0xeb, 0xb2, 0x48, 0x1a,
+                 0xf8, 0x85, 0x80, 0xf6, 0x16, 0x00]; "store path")]
+    #[test_case("0c5b8vw40dy178xlpddw65q9gf1h2186jcc3p4swinwggbllv8mk", vec![
+        0xb3, 0xa2, 0x4d, 0xe9, 0x7a, 0x8f, 0xdb, 0xc8, 0x35, 0xb9, 0x83, 0x31, 0x69, 0x50, 0x10, 0x30,
+        0xb8, 0x97, 0x70, 0x31, 0xbc, 0xb5, 0x4b, 0x3b, 0x3a, 0xc1, 0x37, 0x40, 0xf8, 0x46, 0xab, 0x30,
+    ]; "sha256")]
+    fn encode(enc: &str, dec: Vec<u8>) {
+        assert_eq!(enc, super::encode(&dec));
+    }
+
+    #[test_case("", Some(vec![]) ; "empty bytes")]
+    #[test_case("0z", Some(vec![0x1f]); "one byte")]
+    #[test_case("00bgd045z0d4icpbc2yyz4gx48ak44la", Some(vec![
+                 0x8a, 0x12, 0x32, 0x15, 0x22, 0xfd, 0x91, 0xef, 0xbd, 0x60, 0xeb, 0xb2, 0x48, 0x1a,
+                 0xf8, 0x85, 0x80, 0xf6, 0x16, 0x00]); "store path")]
+    #[test_case("0c5b8vw40dy178xlpddw65q9gf1h2186jcc3p4swinwggbllv8mk", Some(vec![
+        0xb3, 0xa2, 0x4d, 0xe9, 0x7a, 0x8f, 0xdb, 0xc8, 0x35, 0xb9, 0x83, 0x31, 0x69, 0x50, 0x10, 0x30,
+        0xb8, 0x97, 0x70, 0x31, 0xbc, 0xb5, 0x4b, 0x3b, 0x3a, 0xc1, 0x37, 0x40, 0xf8, 0x46, 0xab, 0x30,
+    ]); "sha256")]
+    // this is invalid encoding, because it encodes 10 1-bytes, so the carry
+    // would be 2 1-bytes
+    #[test_case("zz", None; "invalid encoding-1")]
+    // this is an even more specific example - it'd decode as 00000000 11
+    #[test_case("c0", None; "invalid encoding-2")]
+
+    fn decode(enc: &str, dec: Option<Vec<u8>>) {
+        match dec {
+            Some(dec) => {
+                // The decode needs to match what's passed in dec
+                assert_eq!(dec, super::decode(enc.as_bytes()).unwrap());
+            }
+            None => {
+                // the decode needs to be an error
+                assert!(super::decode(enc.as_bytes()).is_err());
+            }
+        }
+    }
+
+    #[test]
+    fn encode_len() {
+        assert_eq!(super::encode_len(20), 32)
+    }
+
+    #[test]
+    fn decode_len() {
+        assert_eq!(super::decode_len(32), 20)
+    }
+}
diff --git a/tvix/nix-compat/src/nixhash/algos.rs b/tvix/nix-compat/src/nixhash/algos.rs
new file mode 100644
index 000000000000..d6b0bf47bdb7
--- /dev/null
+++ b/tvix/nix-compat/src/nixhash/algos.rs
@@ -0,0 +1,39 @@
+use std::fmt::Display;
+
+use serde::{Deserialize, Serialize};
+
+use crate::nixhash::Error;
+
+/// This are the hash algorithms supported by cppnix.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
+pub enum HashAlgo {
+    Md5,
+    Sha1,
+    Sha256,
+    Sha512,
+}
+
+impl Display for HashAlgo {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match &self {
+            HashAlgo::Md5 => write!(f, "md5"),
+            HashAlgo::Sha1 => write!(f, "sha1"),
+            HashAlgo::Sha256 => write!(f, "sha256"),
+            HashAlgo::Sha512 => write!(f, "sha512"),
+        }
+    }
+}
+
+impl TryFrom<&str> for HashAlgo {
+    type Error = Error;
+
+    fn try_from(algo_str: &str) -> Result<Self, Self::Error> {
+        match algo_str {
+            "md5" => Ok(Self::Md5),
+            "sha1" => Ok(Self::Sha1),
+            "sha256" => Ok(Self::Sha256),
+            "sha512" => Ok(Self::Sha512),
+            _ => Err(Error::InvalidAlgo(algo_str.to_string())),
+        }
+    }
+}
diff --git a/tvix/nix-compat/src/nixhash/mod.rs b/tvix/nix-compat/src/nixhash/mod.rs
new file mode 100644
index 000000000000..dccba7a5ba27
--- /dev/null
+++ b/tvix/nix-compat/src/nixhash/mod.rs
@@ -0,0 +1,454 @@
+use crate::nixbase32;
+use data_encoding::{BASE64, BASE64_NOPAD, HEXLOWER};
+use thiserror::Error;
+
+mod algos;
+mod with_mode;
+
+pub use algos::HashAlgo;
+pub use with_mode::NixHashWithMode;
+
+/// Nix allows specifying hashes in various encodings, and magically just
+/// derives the encoding.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct NixHash {
+    pub digest: Vec<u8>,
+
+    pub algo: HashAlgo,
+}
+
+impl NixHash {
+    /// Constructs a new [NixHash] by specifying [HashAlgo] and digest.
+    pub fn new(algo: HashAlgo, digest: Vec<u8>) -> Self {
+        Self { algo, digest }
+    }
+
+    /// Formats a [NixHash] in the Nix default hash format,
+    /// which is the algo, followed by a colon, then the lower hex encoded digest.
+    pub fn to_nix_hash_string(&self) -> String {
+        format!("{}:{}", self.algo, HEXLOWER.encode(&self.digest))
+    }
+}
+
+/// Errors related to NixHash construction.
+#[derive(Debug, Error)]
+pub enum Error {
+    #[error("invalid hash algo: {0}")]
+    InvalidAlgo(String),
+    #[error("invalid SRI string: {0}")]
+    InvalidSRI(String),
+    #[error("invalid encoded digest length '{0}' for algo {1}")]
+    InvalidEncodedDigestLength(usize, HashAlgo),
+    #[error("invalid base16 encoding: {0}")]
+    InvalidBase16Encoding(data_encoding::DecodeError),
+    #[error("invalid base32 encoding: {0}")]
+    InvalidBase32Encoding(nixbase32::Nixbase32DecodeError),
+    #[error("invalid base64 encoding: {0}")]
+    InvalidBase64Encoding(data_encoding::DecodeError),
+    #[error("conflicting hash algo: {0} (hash_algo) vs {1} (inline)")]
+    ConflictingHashAlgos(String, String),
+    #[error("missing inline hash algo, but no externally-specified algo: {0}")]
+    MissingInlineHashAlgo(String),
+}
+
+/// parses a string to a nix hash.
+///
+/// Hashes can be:
+/// - Nix hash strings
+/// - SRI hashes
+/// - bare digests
+///
+/// Encoding for Nix hash strings or bare digests can be:
+/// - base16 (lowerhex),
+/// - nixbase32,
+/// - base64 (StdEncoding)
+/// - sri string
+///
+/// The encoding is derived from the length of the string and the hash type.
+/// The hash is communicated out-of-band, but might also be in-band (in the
+/// case of a nix hash string or SRI), in which it needs to be consistent with the
+/// one communicated out-of-band.
+pub fn from_str(s: &str, algo_str: Option<&str>) -> Result<NixHash, Error> {
+    // validate algo_str, construct hash_algo
+    let algo: Option<HashAlgo> = match &algo_str {
+        Some("sha1") => Some(HashAlgo::Sha1),
+        Some("sha256") => Some(HashAlgo::Sha256),
+        Some("sha512") => Some(HashAlgo::Sha512),
+        Some("md5") => Some(HashAlgo::Md5),
+        Some(e) => return Err(Error::InvalidAlgo(e.to_string())),
+        None => None,
+    };
+
+    // peek at the beginning of the string. Let's detect the SRI path first.
+    if s.starts_with("sha1-")
+        || s.starts_with("sha256-")
+        || s.starts_with("sha512-")
+        || s.starts_with("md5-")
+    {
+        let parsed_nixhash = from_sri_str(s)?;
+        // ensure the algo matches with what has been passed externally, if so.
+        if let Some(algo) = algo {
+            if algo != parsed_nixhash.algo {
+                return Err(Error::ConflictingHashAlgos(
+                    algo.to_string(),
+                    parsed_nixhash.algo.to_string(),
+                ));
+            }
+        }
+        return Ok(parsed_nixhash);
+    }
+
+    // Now, peek at the beginning again to see if it's a Nix Hash
+    if s.starts_with("sha1:")
+        || s.starts_with("sha256:")
+        || s.starts_with("sha512:")
+        || s.starts_with("md5:")
+    {
+        let parsed_nixhash = from_nix_str(s)?;
+        // ensure the algo matches with what has been passed externally, if so.
+        if let Some(algo) = algo {
+            if algo != parsed_nixhash.algo {
+                return Err(Error::ConflictingHashAlgos(
+                    algo.to_string(),
+                    parsed_nixhash.algo.to_string(),
+                ));
+            }
+        }
+        return Ok(parsed_nixhash);
+    }
+
+    // In all other cases, we assume a bare digest, so there MUST be an externally-passed algo.
+    match algo {
+        // Fail if there isn't.
+        None => Err(Error::MissingInlineHashAlgo(s.to_string())),
+        Some(algo) => decode_digest(s, algo),
+    }
+}
+
+/// Parses a Nix hash string ($algo:$digest) to a NixHash.
+pub fn from_nix_str(s: &str) -> Result<NixHash, Error> {
+    if let Some(rest) = s.strip_prefix("sha1:") {
+        decode_digest(rest, HashAlgo::Sha1)
+    } else if let Some(rest) = s.strip_prefix("sha256:") {
+        decode_digest(rest, HashAlgo::Sha256)
+    } else if let Some(rest) = s.strip_prefix("sha512:") {
+        decode_digest(rest, HashAlgo::Sha512)
+    } else if let Some(rest) = s.strip_prefix("md5:") {
+        decode_digest(rest, HashAlgo::Md5)
+    } else {
+        Err(Error::InvalidAlgo(s.to_string()))
+    }
+}
+
+/// Parses a Nix SRI string to a NixHash.
+/// Contrary to the SRI spec, Nix doesn't support SRI strings with multiple hashes,
+/// only supports sha256 and sha512 from the spec, and supports sha1 and md5
+/// additionally.
+/// It also accepts SRI strings where the base64 has an with invalid padding.
+pub fn from_sri_str(s: &str) -> Result<NixHash, Error> {
+    // try to find the first occurence of "-"
+    let idx = s.as_bytes().iter().position(|&e| e == b'-');
+
+    if idx.is_none() {
+        return Err(Error::InvalidSRI(s.to_string()));
+    }
+
+    let idx = idx.unwrap();
+
+    // try to map the part before that `-` to a supported hash algo:
+    let algo: HashAlgo = s[..idx].try_into()?;
+
+    // the rest should be the digest (as Nix doesn't support more than one hash in an SRI string).
+    let encoded_digest = &s[idx + 1..];
+    let actual_len = encoded_digest.as_bytes().len();
+
+    // verify the digest length matches what we'd expect from the hash function,
+    // and then either try decoding as BASE64 or BASE64_NOPAD.
+    // This will also reject SRI strings with more than one hash, because the length won't match
+    if actual_len == BASE64.encode_len(hash_algo_length(&algo)) {
+        let digest: Vec<u8> = BASE64
+            .decode(encoded_digest.as_bytes())
+            .map_err(Error::InvalidBase64Encoding)?;
+        Ok(NixHash { digest, algo })
+    } else if actual_len == BASE64_NOPAD.encode_len(hash_algo_length(&algo)) {
+        let digest: Vec<u8> = BASE64_NOPAD
+            .decode(encoded_digest.as_bytes())
+            .map_err(Error::InvalidBase64Encoding)?;
+        Ok(NixHash { digest, algo })
+    } else {
+        // NOTE: As of now, we reject SRI hashes containing additional
+        // characters (which upstream Nix seems to simply truncate), as
+        // there's no occurence of this is in nixpkgs.
+        // It most likely should also be a bug in Nix.
+        Err(Error::InvalidEncodedDigestLength(
+            encoded_digest.as_bytes().len(),
+            algo,
+        ))
+    }
+}
+
+/// decode a plain digest depending on the hash algo specified externally.
+fn decode_digest(s: &str, algo: HashAlgo) -> Result<NixHash, Error> {
+    // for the chosen hash algo, calculate the expected (decoded) digest length
+    // (as bytes)
+    let expected_digest_len = hash_algo_length(&algo);
+
+    Ok(NixHash {
+        digest: match s.len() {
+            n if n == data_encoding::HEXLOWER.encode_len(expected_digest_len) => {
+                data_encoding::HEXLOWER
+                    .decode(s.as_ref())
+                    .map_err(Error::InvalidBase16Encoding)
+            }
+            n if n == nixbase32::encode_len(expected_digest_len) => {
+                nixbase32::decode(s.as_ref()).map_err(Error::InvalidBase32Encoding)
+            }
+            n if n == BASE64.encode_len(expected_digest_len) => BASE64
+                .decode(s.as_ref())
+                .map_err(Error::InvalidBase64Encoding),
+            _ => return Err(Error::InvalidEncodedDigestLength(s.len(), algo)),
+        }?,
+        algo,
+    })
+}
+
+// return the number of bytes in the digest of the given hash algo.
+fn hash_algo_length(hash_algo: &HashAlgo) -> usize {
+    match hash_algo {
+        HashAlgo::Sha1 => 20,
+        HashAlgo::Sha256 => 32,
+        HashAlgo::Sha512 => 64,
+        HashAlgo::Md5 => 16,
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        nixbase32,
+        nixhash::{self, HashAlgo, NixHash},
+    };
+    use test_case::test_case;
+    const DIGEST_SHA1: &[u8] = &[
+        0x60, 0x16, 0x77, 0x79, 0x97, 0xc3, 0x0a, 0xb0, 0x24, 0x13, 0xcf, 0x50, 0x95, 0x62, 0x2c,
+        0xd7, 0x92, 0x42, 0x83, 0xac,
+    ];
+
+    const DIGEST_SHA256: &[u8] = &[
+        0xa5, 0xce, 0x9c, 0x15, 0x5e, 0xd0, 0x93, 0x97, 0x61, 0x46, 0x46, 0xc9, 0x71, 0x7f, 0xc7,
+        0xcd, 0x94, 0xb1, 0x02, 0x3d, 0x7b, 0x76, 0xb6, 0x18, 0xd4, 0x09, 0xe4, 0xfe, 0xfd, 0x6e,
+        0x9d, 0x39,
+    ];
+
+    const DIGEST_SHA512: &[u8] = &[
+        0xab, 0x40, 0xd0, 0xbe, 0x35, 0x41, 0xf0, 0x77, 0x4b, 0xba, 0x78, 0x15, 0xd1, 0x3d, 0x10,
+        0xb0, 0x32, 0x52, 0xe9, 0x6e, 0x95, 0xf7, 0xdb, 0xb4, 0xee, 0x99, 0xa3, 0xb4, 0x31, 0xc2,
+        0x16, 0x62, 0xfd, 0x69, 0x71, 0xa0, 0x20, 0x16, 0x0e, 0x39, 0x84, 0x8a, 0xa5, 0xf3, 0x05,
+        0xb9, 0xbe, 0x0f, 0x78, 0x72, 0x7b, 0x2b, 0x07, 0x89, 0xe3, 0x9f, 0x12, 0x4d, 0x21, 0xe9,
+        0x2b, 0x8f, 0x39, 0xef,
+    ];
+    const DIGEST_MD5: &[u8] = &[
+        0xc4, 0x87, 0x4a, 0x88, 0x97, 0x44, 0x0b, 0x39, 0x3d, 0x86, 0x2d, 0x8f, 0xd4, 0x59, 0x07,
+        0x3f,
+    ];
+
+    fn to_base16(digest: &[u8]) -> String {
+        data_encoding::HEXLOWER.encode(digest)
+    }
+
+    fn to_nixbase32(digest: &[u8]) -> String {
+        nixbase32::encode(digest)
+    }
+
+    fn to_base64(digest: &[u8]) -> String {
+        data_encoding::BASE64.encode(digest)
+    }
+
+    fn to_base64_nopad(digest: &[u8]) -> String {
+        data_encoding::BASE64_NOPAD.encode(digest)
+    }
+
+    // TODO
+    fn make_nixhash(algo: &HashAlgo, digest_encoded: String) -> String {
+        format!("{}:{}", algo, digest_encoded)
+    }
+    fn make_sri_string(algo: &HashAlgo, digest_encoded: String) -> String {
+        format!("{}-{}", algo, digest_encoded)
+    }
+
+    /// Test parsing a hash string in various formats, and also when/how the out-of-band algo is needed.
+    #[test_case(DIGEST_SHA1, HashAlgo::Sha1; "sha1")]
+    #[test_case(DIGEST_SHA256, HashAlgo::Sha256; "sha256")]
+    #[test_case(DIGEST_SHA512, HashAlgo::Sha512; "sha512")]
+    #[test_case(DIGEST_MD5, HashAlgo::Md5; "md5")]
+    fn from_str(digest: &[u8], algo: HashAlgo) {
+        let expected_hash = NixHash {
+            digest: digest.to_vec(),
+            algo: algo.clone(),
+        };
+        // parse SRI
+        {
+            // base64 without out-of-band algo
+            let s = make_sri_string(&algo, to_base64(digest));
+            let h = nixhash::from_str(&s, None).expect("must succeed");
+            assert_eq!(expected_hash, h);
+
+            // base64 with out-of-band-algo
+            let s = make_sri_string(&algo, to_base64(digest));
+            let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed");
+            assert_eq!(expected_hash, h);
+
+            // base64_nopad without out-of-band algo
+            let s = make_sri_string(&algo, to_base64_nopad(digest));
+            let h = nixhash::from_str(&s, None).expect("must succeed");
+            assert_eq!(expected_hash, h);
+
+            // base64_nopad with out-of-band-algo
+            let s = make_sri_string(&algo, to_base64_nopad(digest));
+            let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed");
+            assert_eq!(expected_hash, h);
+        }
+
+        // parse plain base16. should succeed with algo out-of-band, but fail without.
+        {
+            let s = to_base16(digest);
+            nixhash::from_str(&s, None).expect_err("must fail");
+            let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed");
+            assert_eq!(expected_hash, h);
+        }
+
+        // parse plain nixbase32. should succeed with algo out-of-band, but fail without.
+        {
+            let s = to_nixbase32(digest);
+            nixhash::from_str(&s, None).expect_err("must fail");
+            let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed");
+            assert_eq!(expected_hash, h);
+        }
+
+        // parse plain base64. should succeed with algo out-of-band, but fail without.
+        {
+            let s = to_base64(digest);
+            nixhash::from_str(&s, None).expect_err("must fail");
+            let h = nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed");
+            assert_eq!(expected_hash, h);
+        }
+
+        // parse Nix hash strings
+        {
+            // base16. should succeed with both algo out-of-band and in-band.
+            {
+                let s = make_nixhash(&algo, to_base16(digest));
+                assert_eq!(
+                    expected_hash,
+                    nixhash::from_str(&s, None).expect("must succeed")
+                );
+                assert_eq!(
+                    expected_hash,
+                    nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed")
+                );
+            }
+            // nixbase32. should succeed with both algo out-of-band and in-band.
+            {
+                let s = make_nixhash(&algo, to_nixbase32(digest));
+                assert_eq!(
+                    expected_hash,
+                    nixhash::from_str(&s, None).expect("must succeed")
+                );
+                assert_eq!(
+                    expected_hash,
+                    nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed")
+                );
+            }
+            // base64. should succeed with both algo out-of-band and in-band.
+            {
+                let s = make_nixhash(&algo, to_base64(digest));
+                assert_eq!(
+                    expected_hash,
+                    nixhash::from_str(&s, None).expect("must succeed")
+                );
+                assert_eq!(
+                    expected_hash,
+                    nixhash::from_str(&s, Some(&algo.to_string())).expect("must succeed")
+                );
+            }
+        }
+    }
+
+    /// Test parsing an SRI hash via the [nixhash::from_sri_str] method.
+    #[test]
+    fn from_sri_str() {
+        let nix_hash = nixhash::from_sri_str("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank/v1unTk=")
+            .expect("must succeed");
+
+        assert_eq!(HashAlgo::Sha256, nix_hash.algo);
+        assert_eq!(
+            vec![
+                0xa5, 0xce, 0x9c, 0x15, 0x5e, 0xd0, 0x93, 0x97, 0x61, 0x46, 0x46, 0xc9, 0x71, 0x7f,
+                0xc7, 0xcd, 0x94, 0xb1, 0x02, 0x3d, 0x7b, 0x76, 0xb6, 0x18, 0xd4, 0x09, 0xe4, 0xfe,
+                0xfd, 0x6e, 0x9d, 0x39
+            ],
+            nix_hash.digest
+        )
+    }
+
+    /// Ensure we detect truncated base64 digests, where the digest size
+    /// doesn't match what's expected from that hash function.
+    #[test]
+    fn from_sri_str_truncated() {
+        nixhash::from_sri_str("sha256-pc6cFV7Qk5dhRkbJcX/HzZSxAj17drYY1Ank")
+            .expect_err("must fail");
+    }
+
+    /// Ensure we fail on SRI hashes that Nix doesn't support.
+    #[test]
+    fn from_sri_str_unsupported() {
+        nixhash::from_sri_str(
+            "sha384-o4UVSl89mIB0sFUK+3jQbG+C9Zc9dRlV/Xd3KAvXEbhqxu0J5OAdg6b6VHKHwQ7U",
+        )
+        .expect_err("must fail");
+    }
+
+    /// Ensure we reject invalid base64 encoding
+    #[test]
+    fn from_sri_str_invalid_base64() {
+        nixhash::from_sri_str("sha256-invalid=base64").expect_err("must fail");
+    }
+
+    /// Ensure we reject SRI strings with multiple hashes, as Nix doesn't support that.
+    #[test]
+    fn from_sri_str_unsupported_multiple() {
+        nixhash::from_sri_str("sha256-ngth6szLtC1IJIYyz3lhftzL8SkrJkqPyPve+dGqa1Y= sha512-q0DQvjVB8HdLungV0T0QsDJS6W6V99u07pmjtDHCFmL9aXGgIBYOOYSKpfMFub4PeHJ7KweJ458STSHpK4857w==").expect_err("must fail");
+    }
+
+    /// Nix also accepts SRI strings with missing padding, but only in case the
+    /// string is expressed as SRI, so it still needs to have a `sha256-` prefix.
+    ///
+    /// This both seems to work if it is passed with and without specifying the
+    /// hash algo out-of-band (hash = "sha256-…" or sha256 = "sha256-…")
+    ///
+    /// Passing the same broken base64 string, but not as SRI, while passing
+    /// the hash algo out-of-band does not work.
+    #[test]
+    fn sha256_broken_padding() {
+        let broken_base64 = "fgIr3TyFGDAXP5+qoAaiMKDg/a1MlT6Fv/S/DaA24S8";
+        // if padded with a trailing '='
+        let expected_digest = vec![
+            0x7e, 0x02, 0x2b, 0xdd, 0x3c, 0x85, 0x18, 0x30, 0x17, 0x3f, 0x9f, 0xaa, 0xa0, 0x06,
+            0xa2, 0x30, 0xa0, 0xe0, 0xfd, 0xad, 0x4c, 0x95, 0x3e, 0x85, 0xbf, 0xf4, 0xbf, 0x0d,
+            0xa0, 0x36, 0xe1, 0x2f,
+        ];
+
+        // passing hash algo out of band should succeed
+        let nix_hash = nixhash::from_str(&format!("sha256-{}", &broken_base64), Some("sha256"))
+            .expect("must succeed");
+        assert_eq!(&expected_digest, &nix_hash.digest);
+
+        // not passing hash algo out of band should succeed
+        let nix_hash =
+            nixhash::from_str(&format!("sha256-{}", &broken_base64), None).expect("must succeed");
+        assert_eq!(&expected_digest, &nix_hash.digest);
+
+        // not passing SRI, but hash algo out of band should fail
+        nixhash::from_str(broken_base64, Some("sha256")).expect_err("must fail");
+    }
+}
diff --git a/tvix/nix-compat/src/nixhash/with_mode.rs b/tvix/nix-compat/src/nixhash/with_mode.rs
new file mode 100644
index 000000000000..1908f27b4759
--- /dev/null
+++ b/tvix/nix-compat/src/nixhash/with_mode.rs
@@ -0,0 +1,136 @@
+use crate::nixbase32;
+use crate::nixhash::{HashAlgo, NixHash};
+use serde::ser::SerializeMap;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+pub enum NixHashMode {
+    Flat,
+    Recursive,
+}
+
+impl NixHashMode {
+    pub fn prefix(self) -> &'static str {
+        match self {
+            Self::Flat => "",
+            Self::Recursive => "r:",
+        }
+    }
+}
+
+/// A Nix Hash can either be flat or recursive.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum NixHashWithMode {
+    Flat(NixHash),
+    Recursive(NixHash),
+}
+
+impl NixHashWithMode {
+    pub fn mode(&self) -> NixHashMode {
+        match self {
+            Self::Flat(_) => NixHashMode::Flat,
+            Self::Recursive(_) => NixHashMode::Recursive,
+        }
+    }
+
+    pub fn digest(&self) -> &NixHash {
+        match self {
+            Self::Flat(ref h) => h,
+            Self::Recursive(ref h) => h,
+        }
+    }
+
+    /// Formats a [NixHashWithMode] in the Nix default hash format,
+    /// which is the algo, followed by a colon, then the lower hex encoded digest.
+    /// In case the hash itself is recursive, a `r:` is added as prefix
+    pub fn to_nix_hash_string(&self) -> String {
+        String::from(self.mode().prefix()) + &self.digest().to_nix_hash_string()
+    }
+}
+
+impl Serialize for NixHashWithMode {
+    /// map a NixHashWithMode into the serde data model.
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut map = serializer.serialize_map(Some(2))?;
+        match self {
+            NixHashWithMode::Flat(h) => {
+                map.serialize_entry("hash", &nixbase32::encode(&h.digest))?;
+                map.serialize_entry("hashAlgo", &h.algo.to_string())?;
+            }
+            NixHashWithMode::Recursive(h) => {
+                map.serialize_entry("hash", &nixbase32::encode(&h.digest))?;
+                map.serialize_entry("hashAlgo", &format!("r:{}", &h.algo.to_string()))?;
+            }
+        };
+        map.end()
+    }
+}
+
+impl<'de> Deserialize<'de> for NixHashWithMode {
+    /// map the serde data model into a NixHashWithMode.
+    ///
+    /// The serde data model has a `hash` field (containing a digest in nixbase32),
+    /// and a `hashAlgo` field, containing the stringified hash algo.
+    /// In case the hash is recursive, hashAlgo also has a `r:` prefix.
+    ///
+    /// This is to match how `nix show-derivation` command shows them in JSON
+    /// representation.
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        // TODO: don't use serde_json here?
+        // TODO: serde seems to simply set `hash_with_mode` to None if hash
+        // and hashAlgo fail, but that should be a proper deserialization error
+        // that should be propagated to the user!
+
+        let json = serde_json::Value::deserialize(deserializer)?;
+        match json.as_object() {
+            None => Err(serde::de::Error::custom("couldn't parse as map"))?,
+            Some(map) => {
+                let digest: Vec<u8> = {
+                    if let Some(v) = map.get("hash") {
+                        if let Some(s) = v.as_str() {
+                            data_encoding::HEXLOWER
+                                .decode(s.as_bytes())
+                                .map_err(|e| serde::de::Error::custom(e.to_string()))?
+                        } else {
+                            return Err(serde::de::Error::custom(
+                                "couldn't parse 'hash' as string",
+                            ));
+                        }
+                    } else {
+                        return Err(serde::de::Error::custom("couldn't extract 'hash' key"));
+                    }
+                };
+
+                if let Some(v) = map.get("hashAlgo") {
+                    if let Some(s) = v.as_str() {
+                        match s.strip_prefix("r:") {
+                            Some(rest) => Ok(NixHashWithMode::Recursive(NixHash::new(
+                                HashAlgo::try_from(rest).map_err(|e| {
+                                    serde::de::Error::custom(format!("unable to parse algo: {}", e))
+                                })?,
+                                digest,
+                            ))),
+                            None => Ok(NixHashWithMode::Flat(NixHash::new(
+                                HashAlgo::try_from(s).map_err(|e| {
+                                    serde::de::Error::custom(format!("unable to parse algo: {}", e))
+                                })?,
+                                digest,
+                            ))),
+                        }
+                    } else {
+                        Err(serde::de::Error::custom(
+                            "couldn't parse 'hashAlgo' as string",
+                        ))
+                    }
+                } else {
+                    Err(serde::de::Error::custom("couldn't extract 'hashAlgo' key"))
+                }
+            }
+        }
+    }
+}
diff --git a/tvix/nix-compat/src/store_path/mod.rs b/tvix/nix-compat/src/store_path/mod.rs
new file mode 100644
index 000000000000..a67132cb8e30
--- /dev/null
+++ b/tvix/nix-compat/src/store_path/mod.rs
@@ -0,0 +1,294 @@
+use crate::nixbase32::{self, Nixbase32DecodeError};
+use std::{fmt, path::PathBuf};
+use thiserror::Error;
+
+mod utils;
+
+pub use utils::*;
+
+pub const DIGEST_SIZE: usize = 20;
+// lazy_static doesn't allow us to call NIXBASE32.encode_len(), so we ran it
+// manually and have an assert in the tests.
+pub const ENCODED_DIGEST_SIZE: usize = 32;
+
+// The store dir prefix, without trailing slash.
+// That's usually where the Nix store is mounted at.
+pub const STORE_DIR: &str = "/nix/store";
+pub const STORE_DIR_WITH_SLASH: &str = "/nix/store/";
+
+/// Errors that can occur when parsing a literal store path
+#[derive(Debug, PartialEq, Eq, Error)]
+pub enum Error {
+    #[error("Dash is missing between hash and name")]
+    MissingDash(),
+    #[error("Hash encoding is invalid: {0}")]
+    InvalidHashEncoding(Nixbase32DecodeError),
+    #[error("{0}")]
+    InvalidName(NameError),
+    #[error("Tried to parse an absolute path which was missing the store dir prefix.")]
+    MissingStoreDir(),
+}
+
+/// Errors that can occur during the validation of name characters.
+#[derive(Debug, PartialEq, Eq, Error)]
+pub enum NameError {
+    #[error("Invalid name: {0}")]
+    InvalidName(String),
+}
+
+impl From<NameError> for Error {
+    fn from(e: NameError) -> Self {
+        Self::InvalidName(e)
+    }
+}
+
+/// Represents a path in the Nix store (a direct child of [STORE_DIR]).
+///
+/// It consists of a digest (20 bytes), and a name, which is a string.
+/// The name may only contain ASCII alphanumeric, or one of the following
+/// characters: `-`, `_`, `.`, `+`, `?`, `=`.
+/// The name is usually used to describe the pname and version of a package.
+/// Derivation paths can also be represented as store paths, their names just
+/// end with the `.drv` prefix.
+///
+/// A [StorePath] does not encode any additional subpath "inside" the store
+/// path.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct StorePath {
+    pub digest: [u8; DIGEST_SIZE],
+    pub name: String,
+}
+
+impl StorePath {
+    /// Construct a [StorePath] by passing the `$digest-$name` string
+    /// that comes after [STORE_DIR_WITH_SLASH].
+    pub fn from_string(s: &str) -> Result<StorePath, Error> {
+        // the whole string needs to be at least:
+        //
+        // - 32 characters (encoded hash)
+        // - 1 dash
+        // - 1 character for the name
+        if s.len() < ENCODED_DIGEST_SIZE + 2 {
+            Err(NameError::InvalidName("".to_string()))?;
+        }
+
+        let digest = match nixbase32::decode(s[..ENCODED_DIGEST_SIZE].as_bytes()) {
+            Ok(decoded) => decoded,
+            Err(decoder_error) => return Err(Error::InvalidHashEncoding(decoder_error)),
+        };
+
+        if s.as_bytes()[ENCODED_DIGEST_SIZE] != b'-' {
+            return Err(Error::MissingDash());
+        }
+
+        StorePath::validate_name(&s[ENCODED_DIGEST_SIZE + 2..])?;
+
+        Ok(StorePath {
+            name: s[ENCODED_DIGEST_SIZE + 1..].to_string(),
+            digest: digest.try_into().expect("size is known"),
+        })
+    }
+
+    /// Construct a [StorePath] from an absolute store path string.
+    /// This is equivalent to calling [StorePath::from_string], but stripping
+    /// the [STORE_DIR_WITH_SLASH] prefix before.
+    pub fn from_absolute_path(s: &str) -> Result<StorePath, Error> {
+        match s.strip_prefix(STORE_DIR_WITH_SLASH) {
+            Some(s_stripped) => Self::from_string(s_stripped),
+            None => Err(Error::MissingStoreDir()),
+        }
+    }
+
+    /// Decompose a string into a [StorePath] and a [PathBuf] containing the
+    /// rest of the path, or an error.
+    pub fn from_absolute_path_full(s: &str) -> Result<(StorePath, PathBuf), Error> {
+        // strip [STORE_DIR_WITH_SLASH] from s
+        match s.strip_prefix(STORE_DIR_WITH_SLASH) {
+            None => Err(Error::MissingStoreDir()),
+            Some(rest) => {
+                // put rest in a PathBuf
+                let mut p = PathBuf::new();
+                p.push(rest);
+
+                let mut it = p.components();
+
+                // The first component of the rest must be parse-able as a [StorePath]
+                if let Some(s) = it.next() {
+                    // convert first component to string
+                    if let Some(s) = s.as_os_str().to_str() {
+                        let store_path = StorePath::from_string(s)?;
+                        let rest_buf: PathBuf = it.collect();
+                        Ok((store_path, rest_buf))
+                    } else {
+                        Err(Error::InvalidName(NameError::InvalidName("".to_string())))
+                    }
+                } else {
+                    Err(Error::InvalidName(NameError::InvalidName("".to_string())))
+                }
+            }
+        }
+    }
+
+    /// Converts the [StorePath] to an absolute store path string.
+    /// That is just the string representation, prefixed with the store prefix
+    /// ([STORE_DIR_WITH_SLASH]),
+    pub fn to_absolute_path(&self) -> String {
+        format!("{}{}", STORE_DIR_WITH_SLASH, self)
+    }
+
+    /// Checks a given &str to match the restrictions for store path names.
+    pub fn validate_name(s: &str) -> Result<(), NameError> {
+        for c in s.chars() {
+            if c.is_ascii_alphanumeric()
+                || c == '-'
+                || c == '_'
+                || c == '.'
+                || c == '+'
+                || c == '?'
+                || c == '='
+            {
+                continue;
+            }
+
+            return Err(NameError::InvalidName(s.to_string()));
+        }
+
+        Ok(())
+    }
+}
+
+impl fmt::Display for StorePath {
+    /// The string representation of a store path starts with a digest (20
+    /// bytes), [crate::nixbase32]-encoded, followed by a `-`,
+    /// and ends with the name.
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}-{}", nixbase32::encode(&self.digest), self.name)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::path::PathBuf;
+
+    use crate::nixbase32;
+    use crate::store_path::{DIGEST_SIZE, ENCODED_DIGEST_SIZE};
+    use test_case::test_case;
+
+    use super::{Error, NameError, StorePath};
+
+    #[test]
+    fn encoded_digest_size() {
+        assert_eq!(ENCODED_DIGEST_SIZE, nixbase32::encode_len(DIGEST_SIZE));
+    }
+
+    #[test]
+    fn happy_path() {
+        let example_nix_path_str =
+            "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432";
+        let nixpath =
+            StorePath::from_string(example_nix_path_str).expect("Error parsing example string");
+
+        let expected_digest: [u8; DIGEST_SIZE] = [
+            0x8a, 0x12, 0x32, 0x15, 0x22, 0xfd, 0x91, 0xef, 0xbd, 0x60, 0xeb, 0xb2, 0x48, 0x1a,
+            0xf8, 0x85, 0x80, 0xf6, 0x16, 0x00,
+        ];
+
+        assert_eq!("net-tools-1.60_p20170221182432", nixpath.name);
+        assert_eq!(nixpath.digest, expected_digest);
+
+        assert_eq!(example_nix_path_str, nixpath.to_string())
+    }
+
+    #[test]
+    fn invalid_hash_length() {
+        StorePath::from_string("00bgd045z0d4icpbc2yy-net-tools-1.60_p20170221182432")
+            .expect_err("must fail");
+    }
+
+    #[test]
+    fn invalid_encoding_hash() {
+        StorePath::from_string("00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432")
+            .expect_err("must fail");
+    }
+
+    #[test]
+    fn more_than_just_the_bare_nix_store_path() {
+        StorePath::from_string(
+            "00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432/bin/arp",
+        )
+        .expect_err("must fail");
+    }
+
+    #[test]
+    fn no_dash_between_hash_and_name() {
+        StorePath::from_string("00bgd045z0d4icpbc2yyz4gx48ak44lanet-tools-1.60_p20170221182432")
+            .expect_err("must fail");
+    }
+
+    #[test]
+    fn absolute_path() {
+        let example_nix_path_str =
+            "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432";
+        let nixpath_expected = StorePath::from_string(example_nix_path_str).expect("must parse");
+
+        let nixpath_actual = StorePath::from_absolute_path(
+            "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
+        )
+        .expect("must parse");
+
+        assert_eq!(nixpath_expected, nixpath_actual);
+
+        assert_eq!(
+            "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
+            nixpath_actual.to_absolute_path(),
+        );
+    }
+
+    #[test]
+    fn absolute_path_missing_prefix() {
+        assert_eq!(
+            Error::MissingStoreDir(),
+            StorePath::from_absolute_path("foobar-123").expect_err("must fail")
+        );
+    }
+
+    #[test_case(
+        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
+        (StorePath::from_string("00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::new())
+    ; "without prefix")]
+    #[test_case(
+        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/",
+        (StorePath::from_string("00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::new())
+    ; "without prefix, but trailing slash")]
+    #[test_case(
+        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/bin/arp",
+        (StorePath::from_string("00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::from("bin/arp"))
+    ; "with prefix")]
+    #[test_case(
+        "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432/bin/arp/",
+        (StorePath::from_string("00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432").unwrap(), PathBuf::from("bin/arp/"))
+    ; "with prefix and trailing slash")]
+    fn from_absolute_path_full(s: &str, expected: (StorePath, PathBuf)) {
+        let actual = StorePath::from_absolute_path_full(s).expect("must succeed");
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn from_absolute_path_errors() {
+        assert_eq!(
+            Error::InvalidName(NameError::InvalidName("".to_string())),
+            StorePath::from_absolute_path_full("/nix/store/").expect_err("must fail")
+        );
+        assert_eq!(
+            Error::InvalidName(NameError::InvalidName("".to_string())),
+            StorePath::from_absolute_path_full("/nix/store/foo").expect_err("must fail")
+        );
+        assert_eq!(
+            Error::MissingStoreDir(),
+            StorePath::from_absolute_path_full(
+                "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"
+            )
+            .expect_err("must fail")
+        );
+    }
+}
diff --git a/tvix/nix-compat/src/store_path/utils.rs b/tvix/nix-compat/src/store_path/utils.rs
new file mode 100644
index 000000000000..fd3785568bac
--- /dev/null
+++ b/tvix/nix-compat/src/store_path/utils.rs
@@ -0,0 +1,289 @@
+use crate::nixbase32;
+use crate::nixhash::{HashAlgo, NixHash, NixHashWithMode};
+use crate::store_path::StorePath;
+use sha2::{Digest, Sha256};
+use thiserror::Error;
+
+use super::{NameError, STORE_DIR};
+
+/// Errors that can occur when creating a content-addressed store path.
+///
+/// This wraps the main [Error] which is just about invalid store path names.
+#[derive(Debug, PartialEq, Eq, Error)]
+pub enum BuildStorePathError {
+    #[error("{0}")]
+    InvalidName(NameError),
+    /// This error occurs when we have references outside the SHA-256 +
+    /// Recursive case. The restriction comes from upstream Nix. It may be
+    /// lifted at some point but there isn't a pressing need to anticipate that.
+    #[error("References were not supported as much as requested")]
+    InvalidReference(),
+}
+
+/// compress_hash takes an arbitrarily long sequence of bytes (usually
+/// a hash digest), and returns a sequence of bytes of length
+/// OUTPUT_SIZE.
+///
+/// It's calculated by rotating through the bytes in the output buffer
+/// (zero- initialized), and XOR'ing with each byte of the passed
+/// input. It consumes 1 byte at a time, and XOR's it with the current
+/// value in the output buffer.
+///
+/// This mimics equivalent functionality in C++ Nix.
+pub fn compress_hash<const OUTPUT_SIZE: usize>(input: &[u8]) -> [u8; OUTPUT_SIZE] {
+    let mut output = [0; OUTPUT_SIZE];
+
+    for (ii, ch) in input.iter().enumerate() {
+        output[ii % OUTPUT_SIZE] ^= ch;
+    }
+
+    output
+}
+
+/// This builds a store path, by calculating the text_hash_string of either a
+/// derivation or a literal text file that may contain references.
+pub fn build_text_path<S: AsRef<str>, I: IntoIterator<Item = S>, C: AsRef<[u8]>>(
+    name: &str,
+    content: C,
+    references: I,
+) -> Result<StorePath, NameError> {
+    build_store_path_from_fingerprint_parts(
+        &make_type("text", references, false),
+        // the nix_hash_string representation of the sha256 digest of some contents
+        &{
+            let content_digest = {
+                let hasher = Sha256::new_with_prefix(content);
+                hasher.finalize()
+            };
+            NixHash::new(crate::nixhash::HashAlgo::Sha256, content_digest.to_vec())
+        },
+        name,
+    )
+}
+
+/// This builds a more "regular" content-addressed store path
+pub fn build_regular_ca_path<S: AsRef<str>, I: IntoIterator<Item = S>>(
+    name: &str,
+    hash_with_mode: &NixHashWithMode,
+    references: I,
+    self_reference: bool,
+) -> Result<StorePath, BuildStorePathError> {
+    match &hash_with_mode {
+        NixHashWithMode::Recursive(
+            ref hash @ NixHash {
+                algo: HashAlgo::Sha256,
+                ..
+            },
+        ) => build_store_path_from_fingerprint_parts(
+            &make_type("source", references, self_reference),
+            hash,
+            name,
+        )
+        .map_err(BuildStorePathError::InvalidName),
+        _ => {
+            if references.into_iter().next().is_some() {
+                return Err(BuildStorePathError::InvalidReference());
+            }
+            if self_reference {
+                return Err(BuildStorePathError::InvalidReference());
+            }
+            build_store_path_from_fingerprint_parts(
+                "output:out",
+                &{
+                    let content_digest = {
+                        let mut hasher = Sha256::new_with_prefix("fixed:out:");
+                        hasher.update(hash_with_mode.mode().prefix());
+                        hasher.update(hash_with_mode.digest().algo.to_string());
+                        hasher.update(":");
+                        hasher.update(
+                            &data_encoding::HEXLOWER.encode(&hash_with_mode.digest().digest),
+                        );
+                        hasher.update(":");
+                        hasher.finalize()
+                    };
+                    NixHash::new(crate::nixhash::HashAlgo::Sha256, content_digest.to_vec())
+                },
+                name,
+            )
+            .map_err(BuildStorePathError::InvalidName)
+        }
+    }
+}
+
+/// This builds an input-addressed store path
+///
+/// Input-addresed store paths are always derivation outputs, the "input" in question is the
+/// derivation and its closure.
+pub fn build_output_path(
+    drv_hash: &NixHash,
+    output_name: &str,
+    output_path_name: &str,
+) -> Result<StorePath, NameError> {
+    build_store_path_from_fingerprint_parts(
+        &(String::from("output:") + output_name),
+        drv_hash,
+        output_path_name,
+    )
+}
+
+/// This builds a store path from fingerprint parts.
+/// Usually, that function is used from [build_text_path] and
+/// passed a "text hash string" (starting with "text:" as fingerprint),
+/// but other fingerprints starting with "output:" are also used in Derivation
+/// output path calculation.
+///
+/// The fingerprint is hashed with sha256, its digest is compressed to 20 bytes,
+/// and nixbase32-encoded (32 characters).
+fn build_store_path_from_fingerprint_parts(
+    ty: &str,
+    hash: &NixHash,
+    name: &str,
+) -> Result<StorePath, NameError> {
+    let fingerprint =
+        String::from(ty) + ":" + &hash.to_nix_hash_string() + ":" + STORE_DIR + ":" + name;
+    let digest = {
+        let hasher = Sha256::new_with_prefix(fingerprint);
+        hasher.finalize()
+    };
+    let compressed = compress_hash::<20>(&digest);
+    StorePath::validate_name(name)?;
+    Ok(StorePath {
+        digest: compressed,
+        name: name.to_string(),
+    })
+}
+
+/// This contains the Nix logic to create "text hash strings", which are used
+/// in `builtins.toFile`, as well as in Derivation Path calculation.
+///
+/// A text hash is calculated by concatenating the following fields, separated by a `:`:
+///
+///  - text
+///  - references, individually joined by `:`
+///  - the nix_hash_string representation of the sha256 digest of some contents
+///  - the value of `storeDir`
+///  - the name
+fn make_type<S: AsRef<str>, I: IntoIterator<Item = S>>(
+    ty: &str,
+    references: I,
+    self_ref: bool,
+) -> String {
+    let mut s = String::from(ty);
+
+    for reference in references {
+        s.push(':');
+        s.push_str(reference.as_ref());
+    }
+
+    if self_ref {
+        s.push_str(":self");
+    }
+
+    s
+}
+
+/// Nix placeholders (i.e. values returned by `builtins.placeholder`)
+/// are used to populate outputs with paths that must be
+/// string-replaced with the actual placeholders later, at runtime.
+///
+/// The actual placeholder is basically just a SHA256 hash encoded in
+/// cppnix format.
+pub fn hash_placeholder(name: &str) -> String {
+    let digest = {
+        let mut hasher = Sha256::new();
+        hasher.update(format!("nix-output:{}", name));
+        hasher.finalize()
+    };
+
+    format!("/{}", nixbase32::encode(&digest))
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::nixhash::{NixHash, NixHashWithMode};
+
+    #[test]
+    fn build_text_path_with_zero_references() {
+        // This hash should match `builtins.toFile`, e.g.:
+        //
+        // nix-repl> builtins.toFile "foo" "bar"
+        // "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
+
+        let store_path = build_text_path("foo", "bar", Vec::<String>::new())
+            .expect("build_store_path() should succeed");
+
+        assert_eq!(
+            store_path.to_absolute_path().as_str(),
+            "/nix/store/vxjiwkjkn7x4079qvh1jkl5pn05j2aw0-foo"
+        );
+    }
+
+    #[test]
+    fn build_text_path_with_non_zero_references() {
+        // This hash should match:
+        //
+        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
+        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
+
+        let inner = build_text_path("foo", "bar", Vec::<String>::new())
+            .expect("path_with_references() should succeed");
+        let inner_path = inner.to_absolute_path();
+
+        let outer = build_text_path("baz", &inner_path, vec![inner_path.as_str()])
+            .expect("path_with_references() should succeed");
+
+        assert_eq!(
+            outer.to_absolute_path().as_str(),
+            "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
+        );
+    }
+
+    #[test]
+    fn build_sha1_path() {
+        let outer = build_regular_ca_path(
+            "bar",
+            &NixHashWithMode::Recursive(NixHash {
+                algo: HashAlgo::Sha1,
+                digest: data_encoding::HEXLOWER
+                    .decode(b"0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")
+                    .expect("hex should decode"),
+            }),
+            Vec::<String>::new(),
+            false,
+        )
+        .expect("path_with_references() should succeed");
+
+        assert_eq!(
+            outer.to_absolute_path().as_str(),
+            "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"
+        );
+    }
+
+    #[test]
+    fn build_store_path_with_non_zero_references() {
+        // This hash should match:
+        //
+        // nix-repl> builtins.toFile "baz" "${builtins.toFile "foo" "bar"}"
+        // "/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz"
+        //
+        // $ nix store make-content-addressed /nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz
+        // rewrote '/nix/store/5xd714cbfnkz02h2vbsj4fm03x3f15nf-baz' to '/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz'
+        let outer = build_regular_ca_path(
+            "baz",
+            &NixHashWithMode::Recursive(NixHash {
+                algo: HashAlgo::Sha256,
+                digest: nixbase32::decode(b"1xqkzcb3909fp07qngljr4wcdnrh1gdam1m2n29i6hhrxlmkgkv1")
+                    .expect("hex should decode"),
+            }),
+            vec!["/nix/store/dxwkwjzdaq7ka55pkk252gh32bgpmql4-foo"],
+            false,
+        )
+        .expect("path_with_references() should succeed");
+
+        assert_eq!(
+            outer.to_absolute_path().as_str(),
+            "/nix/store/s89y431zzhmdn3k8r96rvakryddkpv2v-baz"
+        );
+    }
+}
diff --git a/tvix/nix_cli/Cargo.toml b/tvix/nix_cli/Cargo.toml
new file mode 100644
index 000000000000..f9de2b97706b
--- /dev/null
+++ b/tvix/nix_cli/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "nix-cli"
+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 = "4.0"
+
+[dependencies.tempfile]
+version = "3.2.0"
+
+[[bin]]
+name = "nix-store"
+
+[features]
+integration_tests = []
diff --git a/tvix/nix_cli/default.nix b/tvix/nix_cli/default.nix
new file mode 100644
index 000000000000..4c79e46cabfa
--- /dev/null
+++ b/tvix/nix_cli/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.tvix.crates.workspaceMembers.nix-cli.build.override {
+  runTests = true;
+}
diff --git a/tvix/nix_cli/src/bin/nix-store.rs b/tvix/nix_cli/src/bin/nix-store.rs
new file mode 100644
index 000000000000..c6f7d00a4df8
--- /dev/null
+++ b/tvix/nix_cli/src/bin/nix-store.rs
@@ -0,0 +1,105 @@
+fn main() {
+    main_args(std::env::args().collect()).unwrap_or_else(|e| e.exit());
+}
+
+pub fn main_args(args: Vec<String>) -> clap::error::Result<NixResult> {
+    let matches = clap::Command::new("nix-store")
+        .subcommand(clap::Command::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.get_one::<String>("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]
+    #[cfg_attr(not(feature = "integration_tests"), ignore)]
+    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/nix_cli/src/main.rs b/tvix/nix_cli/src/main.rs
new file mode 100644
index 000000000000..40086e6f27ee
--- /dev/null
+++ b/tvix/nix_cli/src/main.rs
@@ -0,0 +1,3 @@
+fn main() {
+    println!("Hello, tvix!");
+}
diff --git a/tvix/proto/LICENSE b/tvix/proto/LICENSE
new file mode 100644
index 000000000000..2034ada6fd9a
--- /dev/null
+++ b/tvix/proto/LICENSE
@@ -0,0 +1,21 @@
+Copyright © 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 000000000000..35e2eba7fed4
--- /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.runCommand "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 000000000000..96431a0b3bfb
--- /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.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/serde/.skip-subtree b/tvix/serde/.skip-subtree
new file mode 100644
index 000000000000..21b2d0d3580d
--- /dev/null
+++ b/tvix/serde/.skip-subtree
@@ -0,0 +1 @@
+The foods.nix can not be read by readTree.
diff --git a/tvix/serde/Cargo.toml b/tvix/serde/Cargo.toml
new file mode 100644
index 000000000000..8ffc11a4eb6e
--- /dev/null
+++ b/tvix/serde/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "tvix-serde"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+tvix-eval = { path = "../eval" }
+serde = { version = "1.0", features = ["derive"] }
diff --git a/tvix/serde/default.nix b/tvix/serde/default.nix
new file mode 100644
index 000000000000..5880bd24f663
--- /dev/null
+++ b/tvix/serde/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.tvix.crates.workspaceMembers.tvix-serde.build.override {
+  runTests = true;
+}
diff --git a/tvix/serde/examples/cfg-demo.rs b/tvix/serde/examples/cfg-demo.rs
new file mode 100644
index 000000000000..5774a81f7752
--- /dev/null
+++ b/tvix/serde/examples/cfg-demo.rs
@@ -0,0 +1,35 @@
+//! This program demonstrates how to use tvix_serde to deserialise
+//! program configuration (or other data) from Nix code.
+//!
+//! This makes it possible to use Nix as an embedded config language.
+//! For greater control over evaluation, and for features like adding
+//! additional builtins, depending directly on tvix_eval would be
+//! required.
+use serde::Deserialize;
+use std::collections::HashMap;
+
+#[derive(Debug, Deserialize)]
+enum Flavour {
+    Tasty,
+    Okay,
+    Eww,
+}
+
+#[allow(dead_code)]
+#[derive(Debug, Deserialize)]
+struct Data {
+    name: String,
+    foods: HashMap<String, Flavour>,
+}
+
+fn main() {
+    // Get the content from wherever, read it from a file, receive it
+    // over the network - whatever floats your boat! We'll include it
+    // as a string.
+    let code = include_str!("foods.nix");
+
+    // Now you can use tvix_serde to deserialise the struct:
+    let foods: Data = tvix_serde::from_str(code).expect("deserialisation should succeed");
+
+    println!("These are the foods:\n{:#?}", foods);
+}
diff --git a/tvix/serde/examples/foods.nix b/tvix/serde/examples/foods.nix
new file mode 100644
index 000000000000..c8733cd3efec
--- /dev/null
+++ b/tvix/serde/examples/foods.nix
@@ -0,0 +1,22 @@
+# This is content for the `Data` struct, written in intentionally
+# convoluted Nix code.
+let
+  mkFlavour = flavour: name: {
+    inherit name;
+    value = flavour;
+  };
+
+  tasty = mkFlavour "Tasty";
+  okay = mkFlavour "Okay";
+  eww = mkFlavour "Eww";
+in
+{
+  name = "exhaustive list of foods";
+
+  foods = builtins.listToAttrs [
+    (tasty "beef")
+    (okay "tomatoes")
+    (eww "olives")
+    (tasty "coffee")
+  ];
+}
diff --git a/tvix/serde/src/de.rs b/tvix/serde/src/de.rs
new file mode 100644
index 000000000000..43efc71c6f67
--- /dev/null
+++ b/tvix/serde/src/de.rs
@@ -0,0 +1,447 @@
+//! Deserialisation from Nix to Rust values.
+
+use serde::de::value::{MapDeserializer, SeqDeserializer};
+use serde::de::{self, EnumAccess, VariantAccess};
+use tvix_eval::Value;
+
+use crate::error::Error;
+
+struct NixDeserializer {
+    value: tvix_eval::Value,
+}
+
+impl NixDeserializer {
+    fn new(value: Value) -> Self {
+        if let Value::Thunk(thunk) = value {
+            Self::new(thunk.value().clone())
+        } else {
+            Self { value }
+        }
+    }
+}
+
+impl de::IntoDeserializer<'_, Error> for NixDeserializer {
+    type Deserializer = Self;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self
+    }
+}
+
+pub fn from_str<'code, T>(src: &'code str) -> Result<T, Error>
+where
+    T: serde::Deserialize<'code>,
+{
+    // First step is to evaluate the Nix code ...
+    let mut eval = tvix_eval::Evaluation::new(src, None);
+    eval.strict = true;
+    let source = eval.source_map();
+    let result = eval.evaluate();
+
+    if !result.errors.is_empty() {
+        return Err(Error::NixErrors {
+            errors: result.errors,
+            source,
+        });
+    }
+
+    let de = NixDeserializer::new(result.value.expect("value should be present on success"));
+
+    T::deserialize(de)
+}
+
+fn unexpected(expected: &'static str, got: &Value) -> Error {
+    Error::UnexpectedType {
+        expected,
+        got: got.type_of(),
+    }
+}
+
+fn visit_integer<I: TryFrom<i64>>(v: &Value) -> Result<I, Error> {
+    match v {
+        Value::Integer(i) => I::try_from(*i).map_err(|_| Error::IntegerConversion {
+            got: *i,
+            need: std::any::type_name::<I>(),
+        }),
+
+        _ => Err(unexpected("integer", v)),
+    }
+}
+
+impl<'de> de::Deserializer<'de> for NixDeserializer {
+    type Error = Error;
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.value {
+            Value::Null => visitor.visit_unit(),
+            Value::Bool(b) => visitor.visit_bool(b),
+            Value::Integer(i) => visitor.visit_i64(i),
+            Value::Float(f) => visitor.visit_f64(f),
+            Value::String(s) => visitor.visit_string(s.to_string()),
+            Value::Path(p) => visitor.visit_string(p.to_string_lossy().into()), // TODO: hmm
+            Value::Attrs(_) => self.deserialize_map(visitor),
+            Value::List(_) => self.deserialize_seq(visitor),
+
+            // tvix-eval types that can not be deserialized through serde.
+            Value::Closure(_)
+            | Value::Builtin(_)
+            | Value::Thunk(_)
+            | Value::AttrNotFound
+            | Value::Blueprint(_)
+            | Value::DeferredUpvalue(_)
+            | Value::UnresolvedPath(_)
+            | Value::Json(_) => Err(Error::Unserializable {
+                value_type: self.value.type_of(),
+            }),
+        }
+    }
+
+    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.value {
+            Value::Bool(b) => visitor.visit_bool(b),
+            _ => Err(unexpected("bool", &self.value)),
+        }
+    }
+
+    fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i8(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i16(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i32(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_i64(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u8(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u16(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u32(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_u64(visit_integer(&self.value)?)
+    }
+
+    fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Float(f) = self.value {
+            return visitor.visit_f32(f as f32);
+        }
+
+        Err(unexpected("float", &self.value))
+    }
+
+    fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Float(f) = self.value {
+            return visitor.visit_f64(f);
+        }
+
+        Err(unexpected("float", &self.value))
+    }
+
+    fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::String(s) = &self.value {
+            let chars = s.as_str().chars().collect::<Vec<_>>();
+            if chars.len() == 1 {
+                return visitor.visit_char(chars[0]);
+            }
+        }
+
+        Err(unexpected("char", &self.value))
+    }
+
+    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::String(s) = &self.value {
+            return visitor.visit_str(s.as_str());
+        }
+
+        Err(unexpected("string", &self.value))
+    }
+
+    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::String(s) = &self.value {
+            return visitor.visit_str(s.as_str());
+        }
+
+        Err(unexpected("string", &self.value))
+    }
+
+    fn deserialize_bytes<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        unimplemented!()
+    }
+
+    fn deserialize_byte_buf<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        unimplemented!()
+    }
+
+    // Note that this can not distinguish between a serialisation of
+    // `Some(())` and `None`.
+    fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Null = self.value {
+            visitor.visit_none()
+        } else {
+            visitor.visit_some(self)
+        }
+    }
+
+    fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Null = self.value {
+            return visitor.visit_unit();
+        }
+
+        Err(unexpected("null", &self.value))
+    }
+
+    fn deserialize_unit_struct<V>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.deserialize_unit(visitor)
+    }
+
+    fn deserialize_newtype_struct<V>(
+        self,
+        _name: &'static str,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_newtype_struct(self)
+    }
+
+    fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::List(list) = self.value {
+            let mut seq =
+                SeqDeserializer::new(list.into_iter().map(|value| NixDeserializer::new(value)));
+            let result = visitor.visit_seq(&mut seq)?;
+            seq.end()?;
+            return Ok(result);
+        }
+
+        Err(unexpected("list", &self.value))
+    }
+
+    fn deserialize_tuple<V>(self, _len: usize, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        // just represent tuples as lists ...
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_tuple_struct<V>(
+        self,
+        _name: &'static str,
+        _len: usize,
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        // same as above
+        self.deserialize_seq(visitor)
+    }
+
+    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        if let Value::Attrs(attrs) = self.value {
+            let mut map = MapDeserializer::new(attrs.into_iter().map(|(k, v)| {
+                (
+                    NixDeserializer::new(Value::String(k)),
+                    NixDeserializer::new(v),
+                )
+            }));
+            let result = visitor.visit_map(&mut map)?;
+            map.end()?;
+            return Ok(result);
+        }
+
+        Err(unexpected("map", &self.value))
+    }
+
+    fn deserialize_struct<V>(
+        self,
+        _name: &'static str,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.deserialize_map(visitor)
+    }
+
+    // This method is responsible for deserializing the externally
+    // tagged enum variant serialisation.
+    fn deserialize_enum<V>(
+        self,
+        name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self.value {
+            // a string represents a unit variant
+            Value::String(s) => visitor.visit_enum(de::value::StrDeserializer::new(s.as_str())),
+
+            // an attribute set however represents an externally
+            // tagged enum with content
+            Value::Attrs(attrs) => visitor.visit_enum(Enum(*attrs)),
+
+            _ => Err(unexpected(name, &self.value)),
+        }
+    }
+
+    fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.deserialize_str(visitor)
+    }
+
+    fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_unit()
+    }
+}
+
+struct Enum(tvix_eval::NixAttrs);
+
+impl<'de> EnumAccess<'de> for Enum {
+    type Error = Error;
+    type Variant = NixDeserializer;
+
+    // TODO: pass the known variants down here and check against them
+    fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
+    where
+        V: de::DeserializeSeed<'de>,
+    {
+        if self.0.len() != 1 {
+            return Err(Error::AmbiguousEnum);
+        }
+
+        let (key, value) = self.0.into_iter().next().expect("length asserted above");
+        let val = seed.deserialize(de::value::StrDeserializer::<Error>::new(key.as_str()))?;
+
+        Ok((val, NixDeserializer::new(value)))
+    }
+}
+
+impl<'de> VariantAccess<'de> for NixDeserializer {
+    type Error = Error;
+
+    fn unit_variant(self) -> Result<(), Self::Error> {
+        // If this case is hit, a user specified the name of a unit
+        // enum variant but gave it content. Unit enum deserialisation
+        // is handled in `deserialize_enum` above.
+        Err(Error::UnitEnumContent)
+    }
+
+    fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
+    where
+        T: de::DeserializeSeed<'de>,
+    {
+        seed.deserialize(self)
+    }
+
+    fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        de::Deserializer::deserialize_seq(self, visitor)
+    }
+
+    fn struct_variant<V>(
+        self,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        de::Deserializer::deserialize_map(self, visitor)
+    }
+}
diff --git a/tvix/serde/src/de_tests.rs b/tvix/serde/src/de_tests.rs
new file mode 100644
index 000000000000..8fe15a17e378
--- /dev/null
+++ b/tvix/serde/src/de_tests.rs
@@ -0,0 +1,200 @@
+use serde::Deserialize;
+use std::collections::HashMap;
+
+use crate::de::from_str;
+
+#[test]
+fn deserialize_none() {
+    let result: Option<usize> = from_str("null").expect("should deserialize");
+    assert_eq!(None, result);
+}
+
+#[test]
+fn deserialize_some() {
+    let result: Option<usize> = from_str("40 + 2").expect("should deserialize");
+    assert_eq!(Some(42), result);
+}
+
+#[test]
+fn deserialize_string() {
+    let result: String = from_str(
+        r#"
+      let greeter = name: "Hello ${name}!";
+      in greeter "Slartibartfast"
+    "#,
+    )
+    .expect("should deserialize");
+
+    assert_eq!(result, "Hello Slartibartfast!");
+}
+
+#[test]
+fn deserialize_empty_list() {
+    let result: Vec<usize> = from_str("[ ]").expect("should deserialize");
+    assert!(result.is_empty())
+}
+
+#[test]
+fn deserialize_integer_list() {
+    let result: Vec<usize> =
+        from_str("builtins.map (n: n + 2) [ 21 40 67 ]").expect("should deserialize");
+    assert_eq!(result, vec![23, 42, 69]);
+}
+
+#[test]
+fn deserialize_empty_map() {
+    let result: HashMap<String, usize> = from_str("{ }").expect("should deserialize");
+    assert!(result.is_empty());
+}
+
+#[test]
+fn deserialize_integer_map() {
+    let result: HashMap<String, usize> = from_str("{ age = 40 + 2; }").expect("should deserialize");
+    assert_eq!(result.len(), 1);
+    assert_eq!(*result.get("age").unwrap(), 42);
+}
+
+#[test]
+fn deserialize_struct() {
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct Person {
+        name: String,
+        age: usize,
+    }
+
+    let result: Person = from_str(
+        r#"
+    {
+      name = "Slartibartfast";
+      age = 42;
+    }
+    "#,
+    )
+    .expect("should deserialize");
+
+    assert_eq!(
+        result,
+        Person {
+            name: "Slartibartfast".into(),
+            age: 42,
+        }
+    );
+}
+
+#[test]
+fn deserialize_newtype() {
+    #[derive(Debug, Deserialize, PartialEq)]
+    struct Number(usize);
+
+    let result: Number = from_str("42").expect("should deserialize");
+    assert_eq!(result, Number(42));
+}
+
+#[test]
+fn deserialize_tuple() {
+    let result: (String, usize) = from_str(r#" [ "foo" 42 ] "#).expect("should deserialize");
+    assert_eq!(result, ("foo".into(), 42));
+}
+
+#[test]
+fn deserialize_unit_enum() {
+    #[derive(Debug, Deserialize, PartialEq)]
+    enum Foo {
+        Bar,
+        Baz,
+    }
+
+    let result: Foo = from_str("\"Baz\"").expect("should deserialize");
+    assert_eq!(result, Foo::Baz);
+}
+
+#[test]
+fn deserialize_tuple_enum() {
+    #[derive(Debug, Deserialize, PartialEq)]
+    enum Foo {
+        Bar,
+        Baz(String, usize),
+    }
+
+    let result: Foo = from_str(
+        r#"
+    {
+      Baz = [ "Slartibartfast" 42 ];
+    }
+    "#,
+    )
+    .expect("should deserialize");
+
+    assert_eq!(result, Foo::Baz("Slartibartfast".into(), 42));
+}
+
+#[test]
+fn deserialize_struct_enum() {
+    #[derive(Debug, Deserialize, PartialEq)]
+    enum Foo {
+        Bar,
+        Baz { name: String, age: usize },
+    }
+
+    let result: Foo = from_str(
+        r#"
+    {
+      Baz.name = "Slartibartfast";
+      Baz.age = 42;
+    }
+    "#,
+    )
+    .expect("should deserialize");
+
+    assert_eq!(
+        result,
+        Foo::Baz {
+            name: "Slartibartfast".into(),
+            age: 42
+        }
+    );
+}
+
+#[test]
+fn deserialize_enum_all() {
+    #[derive(Debug, Deserialize, PartialEq)]
+    #[serde(rename_all = "snake_case")]
+    enum TestEnum {
+        UnitVariant,
+        TupleVariant(String, String),
+        StructVariant { name: String, age: usize },
+    }
+
+    let result: Vec<TestEnum> = from_str(
+        r#"
+      let
+        mkTuple = country: drink: { tuple_variant = [ country drink ]; };
+      in
+      [
+        (mkTuple "UK" "cask ale")
+
+        "unit_variant"
+
+        {
+          struct_variant.name = "Slartibartfast";
+          struct_variant.age = 42;
+        }
+
+        (mkTuple "Russia" "квас")
+      ]
+    "#,
+    )
+    .expect("should deserialize");
+
+    let expected = vec![
+        TestEnum::TupleVariant("UK".into(), "cask ale".into()),
+        TestEnum::UnitVariant,
+        TestEnum::StructVariant {
+            name: "Slartibartfast".into(),
+            age: 42,
+        },
+        TestEnum::TupleVariant("Russia".into(), "квас".into()),
+    ];
+
+    assert_eq!(result, expected);
+}
diff --git a/tvix/serde/src/error.rs b/tvix/serde/src/error.rs
new file mode 100644
index 000000000000..f206b830e95f
--- /dev/null
+++ b/tvix/serde/src/error.rs
@@ -0,0 +1,102 @@
+//! When serialising Nix goes wrong ...
+
+use std::error;
+use std::fmt::Display;
+
+#[derive(Clone, Debug)]
+pub enum Error {
+    /// Attempted to deserialise an unsupported Nix value (such as a
+    /// function) that can not be represented by the
+    /// [`serde::Deserialize`] trait.
+    Unserializable { value_type: &'static str },
+
+    /// Expected to deserialize a value that is unsupported by Nix.
+    Unsupported { wanted: &'static str },
+
+    /// Expected a specific type, but got something else on the Nix side.
+    UnexpectedType {
+        expected: &'static str,
+        got: &'static str,
+    },
+
+    /// Deserialisation error returned from `serde::de`.
+    Deserialization(String),
+
+    /// Deserialized integer did not fit.
+    IntegerConversion { got: i64, need: &'static str },
+
+    /// Evaluation of the supplied Nix code failed while computing the
+    /// value for deserialisation.
+    NixErrors {
+        errors: Vec<tvix_eval::Error>,
+        source: tvix_eval::SourceCode,
+    },
+
+    /// Could not determine an externally tagged enum representation.
+    AmbiguousEnum,
+
+    /// Attempted to provide content to a unit enum.
+    UnitEnumContent,
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::Unserializable { value_type } => write!(
+                f,
+                "can not deserialise a Nix '{}' into a Rust type",
+                value_type
+            ),
+
+            Error::Unsupported { wanted } => {
+                write!(f, "can not deserialize a '{}' from a Nix value", wanted)
+            }
+
+            Error::UnexpectedType { expected, got } => {
+                write!(f, "expected type {}, but got Nix type {}", expected, got)
+            }
+
+            Error::NixErrors { errors, source } => {
+                writeln!(
+                    f,
+                    "{} occured during Nix evaluation: ",
+                    if errors.len() == 1 { "error" } else { "errors" }
+                )?;
+
+                for err in errors {
+                    writeln!(f, "{}", err.fancy_format_str(&source))?;
+                }
+
+                Ok(())
+            }
+
+            Error::Deserialization(err) => write!(f, "deserialisation error occured: {}", err),
+
+            Error::IntegerConversion { got, need } => {
+                write!(f, "i64({}) does not fit in a {}", got, need)
+            }
+
+            Error::AmbiguousEnum => write!(f, "could not determine enum variant: ambiguous keys"),
+
+            Error::UnitEnumContent => write!(f, "provided content for unit enum variant"),
+        }
+    }
+}
+
+impl error::Error for Error {
+    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+        match self {
+            Self::NixErrors { errors, .. } => errors.first().map(|e| e as &dyn error::Error),
+            _ => None,
+        }
+    }
+}
+
+impl serde::de::Error for Error {
+    fn custom<T>(err: T) -> Self
+    where
+        T: Display,
+    {
+        Self::Deserialization(err.to_string())
+    }
+}
diff --git a/tvix/serde/src/lib.rs b/tvix/serde/src/lib.rs
new file mode 100644
index 000000000000..41ad23b18587
--- /dev/null
+++ b/tvix/serde/src/lib.rs
@@ -0,0 +1,11 @@
+//! `tvix-serde` implements (de-)serialisation of Rust data structures
+//! to/from Nix. This is intended to make it easy to use Nix as as
+//! configuration language.
+
+mod de;
+mod error;
+
+pub use de::from_str;
+
+#[cfg(test)]
+mod de_tests;
diff --git a/tvix/store/Cargo.toml b/tvix/store/Cargo.toml
new file mode 100644
index 000000000000..a88bdefd7aac
--- /dev/null
+++ b/tvix/store/Cargo.toml
@@ -0,0 +1,48 @@
+[package]
+name = "tvix-store"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.68"
+blake3 = { version = "1.3.1", features = ["rayon", "std"] }
+clap = { version = "4.0", features = ["derive", "env"] }
+count-write = "0.1.0"
+data-encoding = "2.3.3"
+lazy_static = "1.4.0"
+nix-compat = { path = "../nix-compat" }
+prost = "0.11.2"
+rayon = "1.6.1"
+sha2 = "0.10.6"
+sled = { version = "0.34.7", features = ["compression"] }
+tvix-eval = { path = "../eval" }
+thiserror = "1.0.38"
+tokio-stream = "0.1.14"
+tokio = { version = "1.28.0", features = ["rt-multi-thread", "net"] }
+tonic = "0.8.2"
+tracing = "0.1.37"
+tracing-subscriber = { version = "0.3.16", features = ["json"] }
+walkdir = "2.3.2"
+tokio-util = { version = "0.7.8", features = ["io", "io-util"] }
+tower = "0.4.13"
+futures = "0.3.28"
+bytes = "1.4.0"
+smol_str = "0.2.0"
+serde_json = "1.0"
+
+[dependencies.tonic-reflection]
+optional = true
+version = "0.5.0"
+
+[build-dependencies]
+prost-build = "0.11.2"
+tonic-build = "0.8.2"
+
+[dev-dependencies]
+test-case = "2.2.2"
+tempfile = "3.3.0"
+tonic-mock = { git = "https://github.com/brainrake/tonic-mock", branch = "bump-dependencies" }
+
+[features]
+default = ["reflection"]
+reflection = ["tonic-reflection"]
diff --git a/tvix/store/README.md b/tvix/store/README.md
new file mode 100644
index 000000000000..7844264ca181
--- /dev/null
+++ b/tvix/store/README.md
@@ -0,0 +1,59 @@
+# //tvix/store
+
+This contains the code hosting the tvix-store.
+
+For the local store, Nix realizes files on the filesystem in `/nix/store` (and
+maintains some metadata in a SQLite database). For "remote stores", it
+communicates this metadata in NAR (Nix ARchive) and NARInfo format.
+
+Compared to the Nix model, `tvix-store` stores data on a much more granular
+level than that, which provides more deduplication possibilities, and more
+granular copying.
+
+However, enough information is preserved to still be able to render NAR and
+NARInfo (handled by `//tvix/nar-bridge`).
+
+## More Information
+Check the `protos/` subfolder for the definition of the exact RPC methods and
+messages.
+
+
+## Interacting with the GRPC service manually
+The shell environment in `//tvix` provides `evans`, which is an interactive
+REPL-based gPRC client.
+
+You can use it to connect to a `tvix-store` and call the various RPC methods.
+
+```shell
+$ cargo run &
+$ evans --host localhost --port 8000 -r repl
+  ______
+ |  ____|
+ | |__    __   __   __ _   _ __    ___
+ |  __|   \ \ / /  / _. | | '_ \  / __|
+ | |____   \ V /  | (_| | | | | | \__ \
+ |______|   \_/    \__,_| |_| |_| |___/
+
+ more expressive universal gRPC client
+
+
+tvix.store.v1@localhost:8000> service BlobService
+
+tvix.store.v1.BlobService@localhost:8000> call Put --bytes-from-file
+data (TYPE_BYTES) => /run/current-system/system
+{
+  "digest": "KOM3/IHEx7YfInAnlJpAElYezq0Sxn9fRz7xuClwNfA="
+}
+
+tvix.store.v1.BlobService@localhost:8000> call Get --bytes-as-base64
+digest (TYPE_BYTES) => KOM3/IHEx7YfInAnlJpAElYezq0Sxn9fRz7xuClwNfA=
+{
+  "data": "eDg2XzY0LWxpbnV4"
+}
+
+$ echo eDg2XzY0LWxpbnV4 | base64 -d
+x86_64-linux
+```
+
+Thanks to `tvix-store` providing gRPC Server Reflection (with `reflection`
+feature), you don't need to point `evans` to the `.proto` files.
diff --git a/tvix/store/build.rs b/tvix/store/build.rs
new file mode 100644
index 000000000000..e75f021a2bcf
--- /dev/null
+++ b/tvix/store/build.rs
@@ -0,0 +1,32 @@
+use std::io::Result;
+
+fn main() -> Result<()> {
+    #[allow(unused_mut)]
+    let mut builder = tonic_build::configure();
+
+    #[cfg(feature = "reflection")]
+    {
+        let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
+        let descriptor_path = out_dir.join("tvix.store.v1.bin");
+
+        builder = builder.file_descriptor_set_path(descriptor_path);
+    };
+
+    builder.build_server(true).build_client(true).compile(
+        &[
+            "tvix/store/protos/castore.proto",
+            "tvix/store/protos/pathinfo.proto",
+            "tvix/store/protos/rpc_blobstore.proto",
+            "tvix/store/protos/rpc_directory.proto",
+            "tvix/store/protos/rpc_pathinfo.proto",
+        ],
+        // If we are in running `cargo build` manually, using `../..` works fine,
+        // but in case we run inside a nix build, we need to instead point PROTO_ROOT
+        // to a sparseTree containing that structure.
+        &[match std::env::var_os("PROTO_ROOT") {
+            Some(proto_root) => proto_root.to_str().unwrap().to_owned(),
+            None => "../..".to_string(),
+        }],
+    )?;
+    Ok(())
+}
diff --git a/tvix/store/default.nix b/tvix/store/default.nix
new file mode 100644
index 000000000000..ec04629ab24b
--- /dev/null
+++ b/tvix/store/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.tvix.crates.workspaceMembers.tvix-store.build.override {
+  runTests = true;
+}
diff --git a/tvix/store/docs/api.md b/tvix/store/docs/api.md
new file mode 100644
index 000000000000..549ea703aec8
--- /dev/null
+++ b/tvix/store/docs/api.md
@@ -0,0 +1,280 @@
+tvix-store API
+==============
+
+This document outlines the design of the API exposed by tvix-store, as
+well as other implementations of this store protocol.
+
+The store API has four main consumers:
+
+1. The evaluator (or more correctly, the CLI/coordinator, in the Tvix
+   case) communicates with the store to:
+
+   * Upload files and directories (e.g. from `builtins.path`, or `src = ./path`
+     Nix expressions).
+   * Read files from the store where necessary (e.g. when `nixpkgs` is
+     located in the store, or for IFD).
+
+2. The builder communicates with the store to:
+
+   * Upload files and directories after a build, to persist build artifacts in
+     the store.
+
+3. Tvix clients (such as users that have Tvix installed, or, depending
+   on perspective, builder environments) expect the store to
+   "materialise" on disk to provide a directory layout with store
+   paths.
+
+4. Stores may communicate with other stores, to substitute already built store
+   paths, i.e. a store acts as a binary cache for other stores.
+
+The store API attempts to reuse parts of its API between these three
+consumers by making similarities explicit in the protocol. This leads
+to a protocol that is slightly more complex than a simple "file
+upload/download"-system, but at significantly greater efficiency, both in terms
+of deduplication opportunities as well as granularity.
+
+## The Store model
+
+Contents inside a tvix-store can be grouped into three different message types:
+
+ * Blobs
+ * Directories
+ * PathInfo (see further down)
+
+(check `castore.md` for more detailled field descriptions)
+
+### Blobs
+A blob object contains the literal file contents of regular (or executable)
+files.
+
+### Directory
+A directory object describes the direct children of a directory.
+
+It contains:
+ - name of child regular (or executable) files, and their [blake3][blake3] hash.
+ - name of child symlinks, and their target (as string)
+ - name of child directories, and their [blake3][blake3] hash (forming a Merkle DAG)
+
+### Content-addressed Store Model
+For example, lets consider a directory layout like this, with some
+imaginary hashes of file contents:
+
+```
+.
+├── file-1.txt        hash: 5891b5b522d5df086d0ff0b110fb
+└── nested
+    └── file-2.txt    hash: abc6fd595fc079d3114d4b71a4d8
+```
+
+A hash for the *directory* `nested` can be created by creating the `Directory`
+object:
+
+```json
+{
+  "directories": [],
+  "files": [{
+    "name": "file-2.txt",
+    "digest": "abc6fd595fc079d3114d4b71a4d8",
+    "size": 123,
+  }],
+  "symlink": [],
+}
+```
+
+And then hashing a serialised form of that data structure. We use the blake3
+hash of the canonical protobuf representation. Let's assume the hash was
+`ff0029485729bcde993720749232`.
+
+To create the directory object one layer up, we now refer to our `nested`
+directory object in `directories`, and to `file-1.txt` in `files`:
+
+```json
+{
+  "directories": [{
+    "name": "nested",
+    "digest": "ff0029485729bcde993720749232",
+    "size": 1,
+  }],
+  "files": [{
+    "name": "file-1.txt",
+    "digest": "5891b5b522d5df086d0ff0b110fb",
+    "size": 124,
+  }]
+}
+```
+
+This Merkle DAG of Directory objects, and flat store of blobs can be used to
+describe any file/directory/symlink inside a store path. Due to its content-
+addressed nature, it'll automatically deduplicate (re-)used (sub)directories,
+and allow substitution from any (untrusted) source.
+
+The thing that's now only missing is the metadata to map/"mounting" from the
+content-addressed world to a physical path.
+
+### PathInfo
+As most paths in the Nix store currently are input-addressed [^input-addressed],
+we need something mapping from an input-addressed "output path hash" to the
+contents in the content- addressed world.
+
+That's what `PathInfo` provides. It embeds the root node (Directory, File or
+Symlink) at a given store path.
+
+The root nodes' `name` field is populated with the (base)name inside
+`/nix/store`, so `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-pname-1.2.3`.
+
+The `PathInfo` message also stores references to other store paths, and some
+more NARInfo-specific metadata (signatures, narhash, narsize).
+
+
+## API overview
+
+There's three different services:
+
+### BlobService
+`BlobService` can be used to store and retrieve blobs of data, used to host
+regular file contents.
+
+It is content-addressed, using [blake3][blake3]
+as a hashing function.
+
+As blake3 is a tree hash, there's an opportunity to do
+[verified streaming][bao] of parts of the file,
+which doesn't need to trust any more information than the root hash itself.
+Future extensions of the `BlobService` protocol will enable this.
+
+### DirectoryService
+`DirectoryService` allows lookups (and uploads) of `Directory` messages, and
+whole reference graphs of them.
+
+
+### PathInfoService
+The PathInfo service provides lookups from an output path hash to a `PathInfo`
+message.
+
+## Example flows
+
+Below there are some common usecases of tvix-store, and how the different
+services are used.
+
+###  Upload files and directories
+This needed for `builtins.path` or `src = ./path` in Nix expressions (A), as
+well as for uploading build artifacts to a store (B).
+
+The path specified needs to be (recursively, BFS-style) traversed.
+ * All file contents need to be hashed with blake3, and submitted to the
+   *BlobService* if not already present.
+   A reference to them needs to be added to the parent Directory object that's
+   constructed.
+ * All symlinks need to be added to the parent directory they reside in.
+ * Whenever a Directory has been fully traversed, it needs to be uploaded to
+   the *DirectoryService* and a reference to it needs to be added to the parent
+   Directory object.
+
+Most of the hashing / directory traversal/uploading can happen in parallel,
+as long as Directory objects only refer to Directory objects and Blobs that
+have already been uploaded.
+
+When reaching the root, a `PathInfo` object needs to be constructed.
+
+ * In the case of content-addressed paths (A), the name of the root node is
+   based on the NAR representation of the contents.
+   It might make sense to be able to offload the NAR calculation to the store,
+   which can cache it.
+ * In the case of build artifacts (B), the output path is input-addressed and
+   known upfront.
+
+Contrary to Nix, this has the advantage of not having to upload a lot of things
+to the store that didn't change.
+
+### Reading files from the store from the evaluator
+This is the case when `nixpkgs` is located in the store, or IFD in general.
+
+The store client asks the `PathInfoService` for the `PathInfo` of the output
+path in the request, and looks at the root node.
+
+If something other than the root path is requested, the root_node Directory is
+inspected and potentially a chain of `Directory` objects requested from
+*DirectoryService*. [^n+1query]
+
+When the desired file is reached, the *BlobService* can be used to read the
+contents of this file, and return it back to the evaluator.
+
+FUTUREWORK: define how importing from symlinks should/does work.
+
+Contrary to Nix, this has the advantage of not having to copy all of the
+contents of a store path to the evaluating machine, but really only fetching
+the files the evaluator currently cares about.
+
+### Materializing store paths on disk
+This is useful for people running a Tvix-only system, or running builds on a
+"Tvix remote builder" in its own mount namespace.
+
+In a system with Nix installed, we can't simply manually "extract" things to
+`/nix/store`, as Nix assumes to own all writes to this location.
+In these usecases, we're probably better off exposing a tvix-store as a local
+binary cache (that's what nar-bridge does).
+
+Assuming we are in an environment where we control `/nix/store` exclusively, a
+"realize to disk" would either "extract" things from the tvix-store to a
+filesystem, or expose a FUSE filesystem. The latter would be particularly
+interesting for remote build workloads, as build inputs can be realized on-
+demand, which saves copying around a lot of never-accessed files.
+
+In both cases, the API interactions are similar.
+ * The *PathInfoService* is asked for the `PathInfo` of the requested store path.
+ * If everything should be "extracted", the *DirectoryService* is asked for all
+   `Directory` objects in the closure, the file structure is created, all Blobs
+   are downloaded and placed in their corresponding location and all symlinks
+   are created accordingly.
+ * If this is a FUSE filesystem, we can decide to only request a subset,
+   similar to the "Reading files from the store from the evaluator" usecase,
+   even though it might make sense to keep all Directory objects around.
+   (See the caveat in "Trust model" though!)
+
+### Stores communicating with other stores
+The gRPC API exposed by the tvix-store allows composing multiple stores, and
+implementing some caching strategies, that store clients don't need to be aware
+of.
+
+ * For example, a caching strategy could have a fast local tvix-store, that's
+   asked first and filled with data from a slower remote tvix-store.
+
+ * Multiple stores could be asked for the same data, and whatever store returns
+   the right data first wins.
+
+
+## Trust model / Distribution
+As already described above, the only non-content-addressed service is the
+`PathInfo` service.
+
+This means, all other messages (such as `Blob` and `Directory` messages) can be
+substituted from many different, untrusted sources/mirrors, which will make
+plugging in additional substitution strategies like IPFS, local network
+neighbors super simple.
+
+As for `PathInfo`, we don't specify an additional signature mechanism yet, but
+carry the NAR-based signatures from Nix along.
+
+This means, if we don't trust a remote `PathInfo` object, we currently need to
+"stream" the NAR representation to validate these signatures.
+
+However, the slow part is downloading of NAR files, and considering we have
+more granularity available, we might only need to download some small blobs,
+rather than a whole NAR file.
+
+A future signature mechanism, that is only signing (parts of) the `PathInfo`
+message, which only points to content-addressed data will enable verified
+partial access into a store path, opening up opportunities for lazy filesystem
+access, which is very useful in remote builder scenarios.
+
+
+
+[blake3]: https://github.com/BLAKE3-team/BLAKE3
+[bao]: https://github.com/oconnor663/bao
+[^input-addressed]: Nix hashes the A-Term representation of a .drv, after doing
+                    some replacements on refered Input Derivations to calculate
+                    output paths.
+[^n+1query]: This would expose an N+1 query problem. However it's not a problem
+             in practice, as there's usually always a "local" caching store in
+             the loop, and *DirectoryService* supports a recursive lookup for
+             all `Directory` children of a `Directory`
\ No newline at end of file
diff --git a/tvix/store/docs/castore.md b/tvix/store/docs/castore.md
new file mode 100644
index 000000000000..f555ba5a861b
--- /dev/null
+++ b/tvix/store/docs/castore.md
@@ -0,0 +1,50 @@
+# //tvix/store/docs/castore.md
+
+This provides some more notes on the fields used in castore.proto.
+
+It's meant to supplement `//tvix/store/docs/api.md`.
+
+## Directory message
+`Directory` messages use the blake3 hash of their canonical protobuf
+serialization as its identifier.
+
+A `Directory` message contains three lists, `directories`, `files` and
+`symlinks`, holding `DirectoryNode`, `FileNode` and `SymlinkNode` messages
+respectively. They describe all the direct child elements that are contained in
+a directory.
+
+All three message types have a `name` field, specifying the (base)name of the
+element (which MUST not contain slashes or null bytes, and MUST not be '.' or '..').
+For reproducibility reasons, the lists MUST be sorted by that name and also
+MUST be unique across all three lists.
+
+In addition to the `name` field, the various *Node messages have the following
+fields:
+
+## DirectoryNode
+A `DirectoryNode` message represents a child directory.
+
+It has a `digest` field, which points to the identifier of another `Directory`
+message, making a `Directory` a merkle tree (or strictly speaking, a graph, as
+two elements pointing to a child directory with the same contents would point
+to the same `Directory` message.
+
+There's also a `size` field, containing the (total) number of all child
+elements in the referenced `Directory`, which helps for inode calculation.
+
+## FileNode
+A `FileNode` message represents a child (regular) file.
+
+Its `digest` field contains the blake3 hash of the file contents. It can be
+looked up in the `BlobService`.
+
+The `size` field contains the size of the blob the `digest` field refers to.
+
+The `executable` field specifies whether the file should be marked as
+executable or not.
+
+## SymlinkNode
+A `SymlinkNode` message represents a child symlink.
+
+In addition to the `name` field, the only additional field is the `target`,
+which is a string containing the target of the symlink.
diff --git a/tvix/store/docs/why-not-git-trees.md b/tvix/store/docs/why-not-git-trees.md
new file mode 100644
index 000000000000..fd46252cf55c
--- /dev/null
+++ b/tvix/store/docs/why-not-git-trees.md
@@ -0,0 +1,57 @@
+## Why not git tree objects?
+
+We've been experimenting with (some variations of) the git tree and object
+format, and ultimately decided against using it as an internal format, and
+instead adapted the one documented in the other documents here.
+
+While the tvix-store API protocol shares some similarities with the format used
+in git for trees and objects, the git one has shown some significant
+disadvantages:
+
+### The binary encoding itself
+
+#### trees
+The git tree object format is a very binary, error-prone and
+"made-to-be-read-and-written-from-C" format.
+
+Tree objects are a combination of null-terminated strings, and fields of known
+length. References to other tree objects use the literal sha1 hash of another
+tree object in this encoding.
+Extensions of the format/changes are very hard to do right, because parsers are
+not aware they might be parsing something different.
+
+The tvix-store protocol uses a canonical protobuf serialization, and uses
+the [blake3][blake3] hash of that serialization to point to other `Directory`
+messages.
+It's both compact and with a wide range of libraries for encoders and decoders
+in many programming languages.
+The choice of protobuf makes it easy to add new fields, and make old clients
+aware of some unknown fields being detected [^adding-fields].
+
+#### blob
+On disk, git blob objects start with a "blob" prefix, then the size of the
+payload, and then the data itself. The hash of a blob is the literal sha1sum
+over all of this - which makes it something very git specific to request for.
+
+tvix-store simply uses the [blake3][blake3] hash of the literal contents
+when referring to a file/blob, which makes it very easy to ask other data
+sources for the same data, as no git-specific payload is included in the hash.
+This also plays very well together with things like [iroh][iroh-discussion],
+which plans to provide a way to substitute (large)blobs by their blake3 hash
+over the IPFS network.
+
+In addition to that, [blake3][blake3] makes it possible to do
+[verified streaming][bao], as already described in other parts of the
+documentation.
+
+The git tree object format uses sha1 both for references to other trees and
+hashes of blobs, which isn't really a hash function to fundamentally base
+everything on in 2023.
+The [migration to sha256][git-sha256] also has been dead for some years now,
+and it's unclear how a "blake3" version of this would even look like.
+
+[bao]: https://github.com/oconnor663/bao
+[blake3]: https://github.com/BLAKE3-team/BLAKE3
+[git-sha256]: https://git-scm.com/docs/hash-function-transition/
+[iroh-discussion]: https://github.com/n0-computer/iroh/discussions/707#discussioncomment-5070197
+[^adding-fields]: Obviously, adding new fields will change hashes, but it's something that's easy to detect.
\ No newline at end of file
diff --git a/tvix/store/protos/LICENSE b/tvix/store/protos/LICENSE
new file mode 100644
index 000000000000..2034ada6fd9a
--- /dev/null
+++ b/tvix/store/protos/LICENSE
@@ -0,0 +1,21 @@
+Copyright © 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/store/protos/castore.go b/tvix/store/protos/castore.go
new file mode 100644
index 000000000000..634aa7924f64
--- /dev/null
+++ b/tvix/store/protos/castore.go
@@ -0,0 +1,163 @@
+package storev1
+
+import (
+	"fmt"
+	"strings"
+
+	"google.golang.org/protobuf/proto"
+	"lukechampine.com/blake3"
+)
+
+// The size of a directory is calculated by summing up the numbers of
+// `directories`, `files` and `symlinks`, and for each directory, its size
+// field.
+func (d *Directory) Size() uint32 {
+	var size uint32
+	size = uint32(len(d.Files) + len(d.Symlinks))
+	for _, d := range d.Directories {
+		size += 1 + d.Size
+	}
+	return size
+}
+
+func (d *Directory) Digest() ([]byte, error) {
+	b, err := proto.MarshalOptions{
+		Deterministic: true,
+	}.Marshal(d)
+
+	if err != nil {
+		return nil, fmt.Errorf("error while marshalling directory: %w", err)
+	}
+
+	h := blake3.New(32, nil)
+
+	_, err = h.Write(b)
+	if err != nil {
+		return nil, fmt.Errorf("error writing to hasher: %w", err)
+	}
+
+	return h.Sum(nil), nil
+}
+
+// isValidName checks a name for validity.
+// We disallow slashes, null bytes, '.', '..' and the empty string.
+// Depending on the context, a *Node message with an empty string as name is
+// allowed, but they don't occur inside a Directory message.
+func isValidName(n string) bool {
+	if n == "" || n == ".." || n == "." || strings.Contains(n, "\x00") || strings.Contains(n, "/") {
+		return false
+	}
+	return true
+}
+
+// Validate thecks the Directory message for invalid data, such as:
+// - violations of name restrictions
+// - invalid digest lengths
+// - not properly sorted lists
+// - duplicate names in the three lists
+func (d *Directory) Validate() error {
+	// seenNames contains all seen names so far.
+	// We populate this to ensure node names are unique across all three lists.
+	seenNames := make(map[string]interface{})
+
+	// We also track the last seen name in each of the three lists,
+	// to ensure nodes are sorted by their names.
+	var lastDirectoryName, lastFileName, lastSymlinkName string
+
+	// helper function to only insert in sorted order.
+	// used with the three lists above.
+	// Note this consumes a *pointer to* a string,  as it mutates it.
+	insertIfGt := func(lastName *string, name string) error {
+		// update if it's greater than the previous name
+		if name > *lastName {
+			*lastName = name
+			return nil
+		} else {
+			return fmt.Errorf("%v is not in sorted order", name)
+		}
+	}
+
+	// insertOnce inserts into seenNames if the key doesn't exist yet.
+	insertOnce := func(name string) error {
+		if _, found := seenNames[name]; found {
+			return fmt.Errorf("duplicate name: %v", name)
+		}
+		seenNames[name] = nil
+		return nil
+	}
+
+	// Loop over all Directories, Files and Symlinks individually.
+	// Check the name for validity, check a potential digest for length,
+	// then check for sorting in the current list, and uniqueness across all three lists.
+	for _, directoryNode := range d.Directories {
+		directoryName := directoryNode.GetName()
+
+		// check name for validity
+		if !isValidName(directoryName) {
+			return fmt.Errorf("invalid name for DirectoryNode: %v", directoryName)
+		}
+
+		// check digest to be 32 bytes
+		digestLen := len(directoryNode.GetDigest())
+		if digestLen != 32 {
+			return fmt.Errorf("invalid digest length for DirectoryNode: %d", digestLen)
+		}
+
+		// ensure names are sorted
+		if err := insertIfGt(&lastDirectoryName, directoryName); err != nil {
+			return err
+		}
+
+		// add to seenNames
+		if err := insertOnce(directoryName); err != nil {
+			return err
+		}
+
+	}
+
+	for _, fileNode := range d.Files {
+		fileName := fileNode.GetName()
+
+		// check name for validity
+		if !isValidName(fileName) {
+			return fmt.Errorf("invalid name for FileNode: %v", fileName)
+		}
+
+		// check digest to be 32 bytes
+		digestLen := len(fileNode.GetDigest())
+		if digestLen != 32 {
+			return fmt.Errorf("invalid digest length for FileNode: %d", digestLen)
+		}
+
+		// ensure names are sorted
+		if err := insertIfGt(&lastFileName, fileName); err != nil {
+			return err
+		}
+
+		// add to seenNames
+		if err := insertOnce(fileName); err != nil {
+			return err
+		}
+	}
+
+	for _, symlinkNode := range d.Symlinks {
+		symlinkName := symlinkNode.GetName()
+
+		// check name for validity
+		if !isValidName(symlinkName) {
+			return fmt.Errorf("invalid name for SymlinkNode: %v", symlinkName)
+		}
+
+		// ensure names are sorted
+		if err := insertIfGt(&lastSymlinkName, symlinkName); err != nil {
+			return err
+		}
+
+		// add to seenNames
+		if err := insertOnce(symlinkName); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/tvix/store/protos/castore.pb.go b/tvix/store/protos/castore.pb.go
new file mode 100644
index 000000000000..fab95d2e2c4f
--- /dev/null
+++ b/tvix/store/protos/castore.pb.go
@@ -0,0 +1,450 @@
+// SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+// SPDX-License-Identifier: OSL-3.0 OR MIT OR Apache-2.0
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.29.1
+// 	protoc        (unknown)
+// source: tvix/store/protos/castore.proto
+
+package storev1
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// A Directory can contain Directory, File or Symlink nodes.
+// Each of these nodes have a name attribute, which is the basename in that directory
+// and node type specific attributes.
+// The name attribute:
+//   - MUST not contain slashes or null bytes
+//   - MUST not be '.' or '..'
+//   - MUST be unique across all three lists
+//
+// Elements in each list need to be lexicographically ordered by the name
+// attribute.
+type Directory struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Directories []*DirectoryNode `protobuf:"bytes,1,rep,name=directories,proto3" json:"directories,omitempty"`
+	Files       []*FileNode      `protobuf:"bytes,2,rep,name=files,proto3" json:"files,omitempty"`
+	Symlinks    []*SymlinkNode   `protobuf:"bytes,3,rep,name=symlinks,proto3" json:"symlinks,omitempty"`
+}
+
+func (x *Directory) Reset() {
+	*x = Directory{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_castore_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Directory) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Directory) ProtoMessage() {}
+
+func (x *Directory) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_castore_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Directory.ProtoReflect.Descriptor instead.
+func (*Directory) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_castore_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Directory) GetDirectories() []*DirectoryNode {
+	if x != nil {
+		return x.Directories
+	}
+	return nil
+}
+
+func (x *Directory) GetFiles() []*FileNode {
+	if x != nil {
+		return x.Files
+	}
+	return nil
+}
+
+func (x *Directory) GetSymlinks() []*SymlinkNode {
+	if x != nil {
+		return x.Symlinks
+	}
+	return nil
+}
+
+// A DirectoryNode represents a directory in a Directory.
+type DirectoryNode struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The (base)name of the directory
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// The blake3 hash of a Directory message, serialized in protobuf canonical form.
+	Digest []byte `protobuf:"bytes,2,opt,name=digest,proto3" json:"digest,omitempty"`
+	// Number of child elements in the Directory referred to by `digest`.
+	// Calculated by summing up the numbers of `directories`, `files` and
+	// `symlinks`, and for each directory, its size field. Used for inode
+	// number calculation.
+	// This field is precisely as verifiable as any other Merkle tree edge.
+	// Resolve `digest`, and you can compute it incrementally. Resolve the
+	// entire tree, and you can fully compute it from scratch.
+	// A credulous implementation won't reject an excessive size, but this is
+	// harmless: you'll have some ordinals without nodes. Undersizing is
+	// obvious and easy to reject: you won't have an ordinal for some nodes.
+	Size uint32 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
+}
+
+func (x *DirectoryNode) Reset() {
+	*x = DirectoryNode{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_castore_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *DirectoryNode) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*DirectoryNode) ProtoMessage() {}
+
+func (x *DirectoryNode) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_castore_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use DirectoryNode.ProtoReflect.Descriptor instead.
+func (*DirectoryNode) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_castore_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *DirectoryNode) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *DirectoryNode) GetDigest() []byte {
+	if x != nil {
+		return x.Digest
+	}
+	return nil
+}
+
+func (x *DirectoryNode) GetSize() uint32 {
+	if x != nil {
+		return x.Size
+	}
+	return 0
+}
+
+// A FileNode represents a regular or executable file in a Directory.
+type FileNode struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The (base)name of the file
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// The blake3 digest of the file contents
+	Digest []byte `protobuf:"bytes,2,opt,name=digest,proto3" json:"digest,omitempty"`
+	// The file content size
+	Size uint32 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
+	// Whether the file is executable
+	Executable bool `protobuf:"varint,4,opt,name=executable,proto3" json:"executable,omitempty"`
+}
+
+func (x *FileNode) Reset() {
+	*x = FileNode{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_castore_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FileNode) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FileNode) ProtoMessage() {}
+
+func (x *FileNode) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_castore_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FileNode.ProtoReflect.Descriptor instead.
+func (*FileNode) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_castore_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *FileNode) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *FileNode) GetDigest() []byte {
+	if x != nil {
+		return x.Digest
+	}
+	return nil
+}
+
+func (x *FileNode) GetSize() uint32 {
+	if x != nil {
+		return x.Size
+	}
+	return 0
+}
+
+func (x *FileNode) GetExecutable() bool {
+	if x != nil {
+		return x.Executable
+	}
+	return false
+}
+
+// A SymlinkNode represents a symbolic link in a Directory.
+type SymlinkNode struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The (base)name of the symlink
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	// The target of the symlink.
+	Target string `protobuf:"bytes,2,opt,name=target,proto3" json:"target,omitempty"`
+}
+
+func (x *SymlinkNode) Reset() {
+	*x = SymlinkNode{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_castore_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SymlinkNode) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SymlinkNode) ProtoMessage() {}
+
+func (x *SymlinkNode) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_castore_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SymlinkNode.ProtoReflect.Descriptor instead.
+func (*SymlinkNode) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_castore_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *SymlinkNode) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *SymlinkNode) GetTarget() string {
+	if x != nil {
+		return x.Target
+	}
+	return ""
+}
+
+var File_tvix_store_protos_castore_proto protoreflect.FileDescriptor
+
+var file_tvix_store_protos_castore_proto_rawDesc = []byte{
+	0x0a, 0x1f, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x73, 0x2f, 0x63, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x12, 0x0d, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31,
+	0x22, 0xb2, 0x01, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x3e,
+	0x0a, 0x0b, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20,
+	0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65,
+	0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x4e, 0x6f, 0x64,
+	0x65, 0x52, 0x0b, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x2d,
+	0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e,
+	0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69,
+	0x6c, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x36, 0x0a,
+	0x08, 0x73, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
+	0x1a, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e,
+	0x53, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x4e, 0x6f, 0x64, 0x65, 0x52, 0x08, 0x73, 0x79, 0x6d,
+	0x6c, 0x69, 0x6e, 0x6b, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f,
+	0x72, 0x79, 0x4e, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69,
+	0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65,
+	0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d,
+	0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x6a, 0x0a, 0x08, 0x46, 0x69, 0x6c, 0x65, 0x4e, 0x6f,
+	0x64, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x12,
+	0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69,
+	0x7a, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x61, 0x62, 0x6c, 0x65,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x61, 0x62,
+	0x6c, 0x65, 0x22, 0x39, 0x0a, 0x0b, 0x53, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x4e, 0x6f, 0x64,
+	0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x42, 0x28, 0x5a,
+	0x26, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x74, 0x76, 0x6c, 0x2e, 0x66, 0x79, 0x69, 0x2f, 0x74, 0x76,
+	0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x3b,
+	0x73, 0x74, 0x6f, 0x72, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_tvix_store_protos_castore_proto_rawDescOnce sync.Once
+	file_tvix_store_protos_castore_proto_rawDescData = file_tvix_store_protos_castore_proto_rawDesc
+)
+
+func file_tvix_store_protos_castore_proto_rawDescGZIP() []byte {
+	file_tvix_store_protos_castore_proto_rawDescOnce.Do(func() {
+		file_tvix_store_protos_castore_proto_rawDescData = protoimpl.X.CompressGZIP(file_tvix_store_protos_castore_proto_rawDescData)
+	})
+	return file_tvix_store_protos_castore_proto_rawDescData
+}
+
+var file_tvix_store_protos_castore_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_tvix_store_protos_castore_proto_goTypes = []interface{}{
+	(*Directory)(nil),     // 0: tvix.store.v1.Directory
+	(*DirectoryNode)(nil), // 1: tvix.store.v1.DirectoryNode
+	(*FileNode)(nil),      // 2: tvix.store.v1.FileNode
+	(*SymlinkNode)(nil),   // 3: tvix.store.v1.SymlinkNode
+}
+var file_tvix_store_protos_castore_proto_depIdxs = []int32{
+	1, // 0: tvix.store.v1.Directory.directories:type_name -> tvix.store.v1.DirectoryNode
+	2, // 1: tvix.store.v1.Directory.files:type_name -> tvix.store.v1.FileNode
+	3, // 2: tvix.store.v1.Directory.symlinks:type_name -> tvix.store.v1.SymlinkNode
+	3, // [3:3] is the sub-list for method output_type
+	3, // [3:3] is the sub-list for method input_type
+	3, // [3:3] is the sub-list for extension type_name
+	3, // [3:3] is the sub-list for extension extendee
+	0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_tvix_store_protos_castore_proto_init() }
+func file_tvix_store_protos_castore_proto_init() {
+	if File_tvix_store_protos_castore_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_tvix_store_protos_castore_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Directory); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_castore_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*DirectoryNode); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_castore_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FileNode); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_castore_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*SymlinkNode); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_tvix_store_protos_castore_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   4,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_tvix_store_protos_castore_proto_goTypes,
+		DependencyIndexes: file_tvix_store_protos_castore_proto_depIdxs,
+		MessageInfos:      file_tvix_store_protos_castore_proto_msgTypes,
+	}.Build()
+	File_tvix_store_protos_castore_proto = out.File
+	file_tvix_store_protos_castore_proto_rawDesc = nil
+	file_tvix_store_protos_castore_proto_goTypes = nil
+	file_tvix_store_protos_castore_proto_depIdxs = nil
+}
diff --git a/tvix/store/protos/castore.proto b/tvix/store/protos/castore.proto
new file mode 100644
index 000000000000..747aab08bdf4
--- /dev/null
+++ b/tvix/store/protos/castore.proto
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+// SPDX-License-Identifier: OSL-3.0 OR MIT OR Apache-2.0
+
+syntax = "proto3";
+
+package tvix.store.v1;
+
+option go_package = "code.tvl.fyi/tvix/store/protos;storev1";
+
+// A Directory can contain Directory, File or Symlink nodes.
+// Each of these nodes have a name attribute, which is the basename in that directory
+// and node type specific attributes.
+// The name attribute:
+//  - MUST not contain slashes or null bytes
+//  - MUST not be '.' or '..'
+//  - MUST be unique across all three lists
+// Elements in each list need to be lexicographically ordered by the name
+// attribute.
+message Directory {
+    repeated DirectoryNode directories = 1;
+    repeated FileNode files = 2;
+    repeated SymlinkNode symlinks = 3;
+}
+
+// A DirectoryNode represents a directory in a Directory.
+message DirectoryNode {
+    // The (base)name of the directory
+    string name = 1;
+    // The blake3 hash of a Directory message, serialized in protobuf canonical form.
+    bytes digest = 2;
+    // Number of child elements in the Directory referred to by `digest`.
+    // Calculated by summing up the numbers of `directories`, `files` and
+    // `symlinks`, and for each directory, its size field. Used for inode
+    // number calculation.
+    // This field is precisely as verifiable as any other Merkle tree edge.
+    // Resolve `digest`, and you can compute it incrementally. Resolve the
+    // entire tree, and you can fully compute it from scratch.
+    // A credulous implementation won't reject an excessive size, but this is
+    // harmless: you'll have some ordinals without nodes. Undersizing is
+    // obvious and easy to reject: you won't have an ordinal for some nodes.
+    uint32 size = 3;
+}
+
+// A FileNode represents a regular or executable file in a Directory.
+message FileNode {
+    // The (base)name of the file
+    string name = 1;
+    // The blake3 digest of the file contents
+    bytes digest = 2;
+    // The file content size
+    uint32 size = 3;
+    // Whether the file is executable
+    bool executable = 4;
+}
+
+// A SymlinkNode represents a symbolic link in a Directory.
+message SymlinkNode {
+    // The (base)name of the symlink
+    string name = 1;
+    // The target of the symlink.
+    string target = 2;
+}
diff --git a/tvix/store/protos/castore_test.go b/tvix/store/protos/castore_test.go
new file mode 100644
index 000000000000..61fe535366d0
--- /dev/null
+++ b/tvix/store/protos/castore_test.go
@@ -0,0 +1,271 @@
+package storev1_test
+
+import (
+	"testing"
+
+	storev1pb "code.tvl.fyi/tvix/store/protos"
+	"github.com/stretchr/testify/assert"
+)
+
+var (
+	dummyDigest = []byte{
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00,
+	}
+)
+
+func TestDirectorySize(t *testing.T) {
+	t.Run("empty", func(t *testing.T) {
+		d := storev1pb.Directory{
+			Directories: []*storev1pb.DirectoryNode{},
+			Files:       []*storev1pb.FileNode{},
+			Symlinks:    []*storev1pb.SymlinkNode{},
+		}
+
+		assert.Equal(t, uint32(0), d.Size())
+	})
+
+	t.Run("containing single empty directory", func(t *testing.T) {
+		d := storev1pb.Directory{
+			Directories: []*storev1pb.DirectoryNode{{
+				Name:   "foo",
+				Digest: dummyDigest,
+				Size:   0,
+			}},
+			Files:    []*storev1pb.FileNode{},
+			Symlinks: []*storev1pb.SymlinkNode{},
+		}
+
+		assert.Equal(t, uint32(1), d.Size())
+	})
+
+	t.Run("containing single non-empty directory", func(t *testing.T) {
+		d := storev1pb.Directory{
+			Directories: []*storev1pb.DirectoryNode{{
+				Name:   "foo",
+				Digest: dummyDigest,
+				Size:   4,
+			}},
+			Files:    []*storev1pb.FileNode{},
+			Symlinks: []*storev1pb.SymlinkNode{},
+		}
+
+		assert.Equal(t, uint32(5), d.Size())
+	})
+
+	t.Run("containing single file", func(t *testing.T) {
+		d := storev1pb.Directory{
+			Directories: []*storev1pb.DirectoryNode{},
+			Files: []*storev1pb.FileNode{{
+				Name:       "foo",
+				Digest:     dummyDigest,
+				Size:       42,
+				Executable: false,
+			}},
+			Symlinks: []*storev1pb.SymlinkNode{},
+		}
+
+		assert.Equal(t, uint32(1), d.Size())
+	})
+
+	t.Run("containing single symlink", func(t *testing.T) {
+		d := storev1pb.Directory{
+			Directories: []*storev1pb.DirectoryNode{},
+			Files:       []*storev1pb.FileNode{},
+			Symlinks: []*storev1pb.SymlinkNode{{
+				Name:   "foo",
+				Target: "bar",
+			}},
+		}
+
+		assert.Equal(t, uint32(1), d.Size())
+	})
+
+}
+func TestDirectoryDigest(t *testing.T) {
+	d := storev1pb.Directory{
+		Directories: []*storev1pb.DirectoryNode{},
+		Files:       []*storev1pb.FileNode{},
+		Symlinks:    []*storev1pb.SymlinkNode{},
+	}
+
+	dgst, err := d.Digest()
+	assert.NoError(t, err, "calling Digest() on a directory shouldn't error")
+	assert.Equal(t, []byte{
+		0xaf, 0x13, 0x49, 0xb9, 0xf5, 0xf9, 0xa1, 0xa6, 0xa0, 0x40, 0x4d, 0xea, 0x36, 0xdc,
+		0xc9, 0x49, 0x9b, 0xcb, 0x25, 0xc9, 0xad, 0xc1, 0x12, 0xb7, 0xcc, 0x9a, 0x93, 0xca,
+		0xe4, 0x1f, 0x32, 0x62,
+	}, dgst)
+}
+
+func TestDirectoryValidate(t *testing.T) {
+	t.Run("empty", func(t *testing.T) {
+		d := storev1pb.Directory{
+			Directories: []*storev1pb.DirectoryNode{},
+			Files:       []*storev1pb.FileNode{},
+			Symlinks:    []*storev1pb.SymlinkNode{},
+		}
+
+		assert.NoError(t, d.Validate())
+	})
+
+	t.Run("invalid names", func(t *testing.T) {
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{{
+					Name:   "",
+					Digest: dummyDigest,
+					Size:   42,
+				}},
+				Files:    []*storev1pb.FileNode{},
+				Symlinks: []*storev1pb.SymlinkNode{},
+			}
+
+			assert.ErrorContains(t, d.Validate(), "invalid name")
+		}
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{{
+					Name:   ".",
+					Digest: dummyDigest,
+					Size:   42,
+				}},
+				Files:    []*storev1pb.FileNode{},
+				Symlinks: []*storev1pb.SymlinkNode{},
+			}
+
+			assert.ErrorContains(t, d.Validate(), "invalid name")
+		}
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{},
+				Files: []*storev1pb.FileNode{{
+					Name:       "..",
+					Digest:     dummyDigest,
+					Size:       42,
+					Executable: false,
+				}},
+				Symlinks: []*storev1pb.SymlinkNode{},
+			}
+
+			assert.ErrorContains(t, d.Validate(), "invalid name")
+		}
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{},
+				Files:       []*storev1pb.FileNode{},
+				Symlinks: []*storev1pb.SymlinkNode{{
+					Name:   "\x00",
+					Target: "foo",
+				}},
+			}
+
+			assert.ErrorContains(t, d.Validate(), "invalid name")
+		}
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{},
+				Files:       []*storev1pb.FileNode{},
+				Symlinks: []*storev1pb.SymlinkNode{{
+					Name:   "foo/bar",
+					Target: "foo",
+				}},
+			}
+
+			assert.ErrorContains(t, d.Validate(), "invalid name")
+		}
+	})
+
+	t.Run("invalid digest", func(t *testing.T) {
+		d := storev1pb.Directory{
+			Directories: []*storev1pb.DirectoryNode{{
+				Name:   "foo",
+				Digest: nil,
+				Size:   42,
+			}},
+			Files:    []*storev1pb.FileNode{},
+			Symlinks: []*storev1pb.SymlinkNode{},
+		}
+
+		assert.ErrorContains(t, d.Validate(), "invalid digest length")
+	})
+
+	t.Run("sorting", func(t *testing.T) {
+		// "b" comes before "a", bad.
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{{
+					Name:   "b",
+					Digest: dummyDigest,
+					Size:   42,
+				}, {
+					Name:   "a",
+					Digest: dummyDigest,
+					Size:   42,
+				}},
+				Files:    []*storev1pb.FileNode{},
+				Symlinks: []*storev1pb.SymlinkNode{},
+			}
+			assert.ErrorContains(t, d.Validate(), "is not in sorted order")
+		}
+
+		// "a" exists twice, bad.
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{{
+					Name:   "a",
+					Digest: dummyDigest,
+					Size:   42,
+				}},
+				Files: []*storev1pb.FileNode{{
+					Name:       "a",
+					Digest:     dummyDigest,
+					Size:       42,
+					Executable: false,
+				}},
+				Symlinks: []*storev1pb.SymlinkNode{},
+			}
+			assert.ErrorContains(t, d.Validate(), "duplicate name")
+		}
+
+		// "a" comes before "b", all good.
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{{
+					Name:   "a",
+					Digest: dummyDigest,
+					Size:   42,
+				}, {
+					Name:   "b",
+					Digest: dummyDigest,
+					Size:   42,
+				}},
+				Files:    []*storev1pb.FileNode{},
+				Symlinks: []*storev1pb.SymlinkNode{},
+			}
+			assert.NoError(t, d.Validate(), "shouldn't error")
+		}
+
+		// [b, c] and [a] are both properly sorted.
+		{
+			d := storev1pb.Directory{
+				Directories: []*storev1pb.DirectoryNode{{
+					Name:   "b",
+					Digest: dummyDigest,
+					Size:   42,
+				}, {
+					Name:   "c",
+					Digest: dummyDigest,
+					Size:   42,
+				}},
+				Files: []*storev1pb.FileNode{},
+				Symlinks: []*storev1pb.SymlinkNode{{
+					Name:   "a",
+					Target: "foo",
+				}},
+			}
+			assert.NoError(t, d.Validate(), "shouldn't error")
+		}
+	})
+}
diff --git a/tvix/store/protos/default.nix b/tvix/store/protos/default.nix
new file mode 100644
index 000000000000..0ffdcac0418a
--- /dev/null
+++ b/tvix/store/protos/default.nix
@@ -0,0 +1,12 @@
+# Target containing just the proto files.
+
+{ depot, lib, ... }:
+
+let
+  inherit (lib.strings) hasSuffix;
+  inherit (builtins) attrNames filter readDir;
+
+  protoFileNames = filter (hasSuffix ".proto") (attrNames (readDir ./.));
+  protoFiles = map (f: ./. + ("/" + f)) protoFileNames;
+in
+depot.nix.sparseTree depot.path.origSrc protoFiles
diff --git a/tvix/store/protos/go.mod b/tvix/store/protos/go.mod
new file mode 100644
index 000000000000..8a21555eaad5
--- /dev/null
+++ b/tvix/store/protos/go.mod
@@ -0,0 +1,19 @@
+module code.tvl.fyi/tvix/store/protos
+
+go 1.19
+
+require (
+	github.com/davecgh/go-spew v1.1.1 // indirect
+	github.com/golang/protobuf v1.5.2 // indirect
+	github.com/klauspost/cpuid/v2 v2.0.9 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/stretchr/testify v1.8.1 // indirect
+	golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
+	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
+	golang.org/x/text v0.4.0 // indirect
+	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
+	google.golang.org/grpc v1.51.0 // indirect
+	google.golang.org/protobuf v1.28.1 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
+	lukechampine.com/blake3 v1.1.7 // indirect
+)
diff --git a/tvix/store/protos/go.sum b/tvix/store/protos/go.sum
new file mode 100644
index 000000000000..7a603cdb120d
--- /dev/null
+++ b/tvix/store/protos/go.sum
@@ -0,0 +1,96 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+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.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+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.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+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/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.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+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-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+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-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-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+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-20190423024810-112230192c58/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-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/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-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+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/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.51.0 h1:E1eGv1FTqoLIdnBCZufiSHgKjlqG6fKFf6pPWtMTh8U=
+google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww=
+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.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+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.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/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-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
+lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
diff --git a/tvix/store/protos/pathinfo.pb.go b/tvix/store/protos/pathinfo.pb.go
new file mode 100644
index 000000000000..f8c66014eaa7
--- /dev/null
+++ b/tvix/store/protos/pathinfo.pb.go
@@ -0,0 +1,498 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.29.1
+// 	protoc        (unknown)
+// source: tvix/store/protos/pathinfo.proto
+
+package storev1
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// PathInfo shows information about a Nix Store Path.
+// That's a single element inside /nix/store.
+type PathInfo struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The path can be a directory, file or symlink.
+	Node *Node `protobuf:"bytes,1,opt,name=node,proto3" json:"node,omitempty"`
+	// List of references (output path hashes)
+	// This really is the raw *bytes*, after decoding nixbase32, and not a
+	// base32-encoded string.
+	References [][]byte `protobuf:"bytes,2,rep,name=references,proto3" json:"references,omitempty"`
+	// see below.
+	Narinfo *NARInfo `protobuf:"bytes,3,opt,name=narinfo,proto3" json:"narinfo,omitempty"`
+}
+
+func (x *PathInfo) Reset() {
+	*x = PathInfo{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PathInfo) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PathInfo) ProtoMessage() {}
+
+func (x *PathInfo) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PathInfo.ProtoReflect.Descriptor instead.
+func (*PathInfo) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_pathinfo_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *PathInfo) GetNode() *Node {
+	if x != nil {
+		return x.Node
+	}
+	return nil
+}
+
+func (x *PathInfo) GetReferences() [][]byte {
+	if x != nil {
+		return x.References
+	}
+	return nil
+}
+
+func (x *PathInfo) GetNarinfo() *NARInfo {
+	if x != nil {
+		return x.Narinfo
+	}
+	return nil
+}
+
+type Node struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Types that are assignable to Node:
+	//
+	//	*Node_Directory
+	//	*Node_File
+	//	*Node_Symlink
+	Node isNode_Node `protobuf_oneof:"node"`
+}
+
+func (x *Node) Reset() {
+	*x = Node{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Node) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Node) ProtoMessage() {}
+
+func (x *Node) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Node.ProtoReflect.Descriptor instead.
+func (*Node) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_pathinfo_proto_rawDescGZIP(), []int{1}
+}
+
+func (m *Node) GetNode() isNode_Node {
+	if m != nil {
+		return m.Node
+	}
+	return nil
+}
+
+func (x *Node) GetDirectory() *DirectoryNode {
+	if x, ok := x.GetNode().(*Node_Directory); ok {
+		return x.Directory
+	}
+	return nil
+}
+
+func (x *Node) GetFile() *FileNode {
+	if x, ok := x.GetNode().(*Node_File); ok {
+		return x.File
+	}
+	return nil
+}
+
+func (x *Node) GetSymlink() *SymlinkNode {
+	if x, ok := x.GetNode().(*Node_Symlink); ok {
+		return x.Symlink
+	}
+	return nil
+}
+
+type isNode_Node interface {
+	isNode_Node()
+}
+
+type Node_Directory struct {
+	Directory *DirectoryNode `protobuf:"bytes,1,opt,name=directory,proto3,oneof"`
+}
+
+type Node_File struct {
+	File *FileNode `protobuf:"bytes,2,opt,name=file,proto3,oneof"`
+}
+
+type Node_Symlink struct {
+	Symlink *SymlinkNode `protobuf:"bytes,3,opt,name=symlink,proto3,oneof"`
+}
+
+func (*Node_Directory) isNode_Node() {}
+
+func (*Node_File) isNode_Node() {}
+
+func (*Node_Symlink) isNode_Node() {}
+
+// Nix C++ uses NAR (Nix Archive) as a format to transfer store paths,
+// and stores metadata and signatures in NARInfo files.
+// Store all these attributes in a separate message.
+//
+// This is useful to render .narinfo files to clients, or to preserve/validate
+// these signatures.
+// As verifying these signatures requires the whole NAR file to be synthesized,
+// moving to another signature scheme is desired.
+// Even then, it still makes sense to hold this data, for old clients.
+type NARInfo struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// This size of the NAR file, in bytes.
+	NarSize uint64 `protobuf:"varint,1,opt,name=nar_size,json=narSize,proto3" json:"nar_size,omitempty"`
+	// The sha256 of the NAR file representation.
+	NarSha256 []byte `protobuf:"bytes,2,opt,name=nar_sha256,json=narSha256,proto3" json:"nar_sha256,omitempty"`
+	// The signatures in a .narinfo file.
+	Signatures []*NARInfo_Signature `protobuf:"bytes,3,rep,name=signatures,proto3" json:"signatures,omitempty"`
+	// A list of references. To validate .narinfo signatures, a fingerprint
+	// needs to be constructed.
+	// This fingerprint doesn't just contain the hashes of the output paths of
+	// all references (like PathInfo.references), but their whole (base)names,
+	// so we need to keep them somewhere.
+	ReferenceNames []string `protobuf:"bytes,4,rep,name=reference_names,json=referenceNames,proto3" json:"reference_names,omitempty"`
+}
+
+func (x *NARInfo) Reset() {
+	*x = NARInfo{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *NARInfo) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*NARInfo) ProtoMessage() {}
+
+func (x *NARInfo) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use NARInfo.ProtoReflect.Descriptor instead.
+func (*NARInfo) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_pathinfo_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *NARInfo) GetNarSize() uint64 {
+	if x != nil {
+		return x.NarSize
+	}
+	return 0
+}
+
+func (x *NARInfo) GetNarSha256() []byte {
+	if x != nil {
+		return x.NarSha256
+	}
+	return nil
+}
+
+func (x *NARInfo) GetSignatures() []*NARInfo_Signature {
+	if x != nil {
+		return x.Signatures
+	}
+	return nil
+}
+
+func (x *NARInfo) GetReferenceNames() []string {
+	if x != nil {
+		return x.ReferenceNames
+	}
+	return nil
+}
+
+// This represents a (parsed) signature line in a .narinfo file.
+type NARInfo_Signature struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
+}
+
+func (x *NARInfo_Signature) Reset() {
+	*x = NARInfo_Signature{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *NARInfo_Signature) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*NARInfo_Signature) ProtoMessage() {}
+
+func (x *NARInfo_Signature) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_pathinfo_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use NARInfo_Signature.ProtoReflect.Descriptor instead.
+func (*NARInfo_Signature) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_pathinfo_proto_rawDescGZIP(), []int{2, 0}
+}
+
+func (x *NARInfo_Signature) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *NARInfo_Signature) GetData() []byte {
+	if x != nil {
+		return x.Data
+	}
+	return nil
+}
+
+var File_tvix_store_protos_pathinfo_proto protoreflect.FileDescriptor
+
+var file_tvix_store_protos_pathinfo_proto_rawDesc = []byte{
+	0x0a, 0x20, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x73, 0x2f, 0x70, 0x61, 0x74, 0x68, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x12, 0x0d, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76,
+	0x31, 0x1a, 0x1f, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x61, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x22, 0x85, 0x01, 0x0a, 0x08, 0x50, 0x61, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12,
+	0x27, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e,
+	0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f,
+	0x64, 0x65, 0x52, 0x04, 0x6e, 0x6f, 0x64, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x65,
+	0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x0a, 0x72, 0x65,
+	0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x07, 0x6e, 0x61, 0x72, 0x69,
+	0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x76, 0x69, 0x78,
+	0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x41, 0x52, 0x49, 0x6e, 0x66,
+	0x6f, 0x52, 0x07, 0x6e, 0x61, 0x72, 0x69, 0x6e, 0x66, 0x6f, 0x22, 0xb3, 0x01, 0x0a, 0x04, 0x4e,
+	0x6f, 0x64, 0x65, 0x12, 0x3c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74,
+	0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79,
+	0x4e, 0x6f, 0x64, 0x65, 0x48, 0x00, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72,
+	0x79, 0x12, 0x2d, 0x0a, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x17, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e,
+	0x46, 0x69, 0x6c, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x48, 0x00, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65,
+	0x12, 0x36, 0x0a, 0x07, 0x73, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28,
+	0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76,
+	0x31, 0x2e, 0x53, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x4e, 0x6f, 0x64, 0x65, 0x48, 0x00, 0x52,
+	0x07, 0x73, 0x79, 0x6d, 0x6c, 0x69, 0x6e, 0x6b, 0x42, 0x06, 0x0a, 0x04, 0x6e, 0x6f, 0x64, 0x65,
+	0x22, 0xe3, 0x01, 0x0a, 0x07, 0x4e, 0x41, 0x52, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08,
+	0x6e, 0x61, 0x72, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07,
+	0x6e, 0x61, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x61, 0x72, 0x5f, 0x73,
+	0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6e, 0x61, 0x72,
+	0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x12, 0x40, 0x0a, 0x0a, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74,
+	0x75, 0x72, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x76, 0x69,
+	0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x41, 0x52, 0x49, 0x6e,
+	0x66, 0x6f, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x52, 0x0a, 0x73, 0x69,
+	0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x72, 0x65, 0x66, 0x65,
+	0x72, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x0e, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65,
+	0x73, 0x1a, 0x33, 0x0a, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x12,
+	0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
+	0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x42, 0x28, 0x5a, 0x26, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x74,
+	0x76, 0x6c, 0x2e, 0x66, 0x79, 0x69, 0x2f, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72,
+	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x76, 0x31,
+	0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_tvix_store_protos_pathinfo_proto_rawDescOnce sync.Once
+	file_tvix_store_protos_pathinfo_proto_rawDescData = file_tvix_store_protos_pathinfo_proto_rawDesc
+)
+
+func file_tvix_store_protos_pathinfo_proto_rawDescGZIP() []byte {
+	file_tvix_store_protos_pathinfo_proto_rawDescOnce.Do(func() {
+		file_tvix_store_protos_pathinfo_proto_rawDescData = protoimpl.X.CompressGZIP(file_tvix_store_protos_pathinfo_proto_rawDescData)
+	})
+	return file_tvix_store_protos_pathinfo_proto_rawDescData
+}
+
+var file_tvix_store_protos_pathinfo_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_tvix_store_protos_pathinfo_proto_goTypes = []interface{}{
+	(*PathInfo)(nil),          // 0: tvix.store.v1.PathInfo
+	(*Node)(nil),              // 1: tvix.store.v1.Node
+	(*NARInfo)(nil),           // 2: tvix.store.v1.NARInfo
+	(*NARInfo_Signature)(nil), // 3: tvix.store.v1.NARInfo.Signature
+	(*DirectoryNode)(nil),     // 4: tvix.store.v1.DirectoryNode
+	(*FileNode)(nil),          // 5: tvix.store.v1.FileNode
+	(*SymlinkNode)(nil),       // 6: tvix.store.v1.SymlinkNode
+}
+var file_tvix_store_protos_pathinfo_proto_depIdxs = []int32{
+	1, // 0: tvix.store.v1.PathInfo.node:type_name -> tvix.store.v1.Node
+	2, // 1: tvix.store.v1.PathInfo.narinfo:type_name -> tvix.store.v1.NARInfo
+	4, // 2: tvix.store.v1.Node.directory:type_name -> tvix.store.v1.DirectoryNode
+	5, // 3: tvix.store.v1.Node.file:type_name -> tvix.store.v1.FileNode
+	6, // 4: tvix.store.v1.Node.symlink:type_name -> tvix.store.v1.SymlinkNode
+	3, // 5: tvix.store.v1.NARInfo.signatures:type_name -> tvix.store.v1.NARInfo.Signature
+	6, // [6:6] is the sub-list for method output_type
+	6, // [6:6] is the sub-list for method input_type
+	6, // [6:6] is the sub-list for extension type_name
+	6, // [6:6] is the sub-list for extension extendee
+	0, // [0:6] is the sub-list for field type_name
+}
+
+func init() { file_tvix_store_protos_pathinfo_proto_init() }
+func file_tvix_store_protos_pathinfo_proto_init() {
+	if File_tvix_store_protos_pathinfo_proto != nil {
+		return
+	}
+	file_tvix_store_protos_castore_proto_init()
+	if !protoimpl.UnsafeEnabled {
+		file_tvix_store_protos_pathinfo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PathInfo); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_pathinfo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Node); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_pathinfo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*NARInfo); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_pathinfo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*NARInfo_Signature); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	file_tvix_store_protos_pathinfo_proto_msgTypes[1].OneofWrappers = []interface{}{
+		(*Node_Directory)(nil),
+		(*Node_File)(nil),
+		(*Node_Symlink)(nil),
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_tvix_store_protos_pathinfo_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   4,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_tvix_store_protos_pathinfo_proto_goTypes,
+		DependencyIndexes: file_tvix_store_protos_pathinfo_proto_depIdxs,
+		MessageInfos:      file_tvix_store_protos_pathinfo_proto_msgTypes,
+	}.Build()
+	File_tvix_store_protos_pathinfo_proto = out.File
+	file_tvix_store_protos_pathinfo_proto_rawDesc = nil
+	file_tvix_store_protos_pathinfo_proto_goTypes = nil
+	file_tvix_store_protos_pathinfo_proto_depIdxs = nil
+}
diff --git a/tvix/store/protos/pathinfo.proto b/tvix/store/protos/pathinfo.proto
new file mode 100644
index 000000000000..896d4aa225ac
--- /dev/null
+++ b/tvix/store/protos/pathinfo.proto
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+syntax = "proto3";
+
+package tvix.store.v1;
+
+import "tvix/store/protos/castore.proto";
+
+option go_package = "code.tvl.fyi/tvix/store/protos;storev1";
+
+// PathInfo shows information about a Nix Store Path.
+// That's a single element inside /nix/store.
+message PathInfo {
+    // The path can be a directory, file or symlink.
+    Node node = 1;
+
+    // List of references (output path hashes)
+    // This really is the raw *bytes*, after decoding nixbase32, and not a
+    // base32-encoded string.
+    repeated bytes references = 2;
+
+    // see below.
+    NARInfo narinfo = 3;
+}
+
+message Node {
+    oneof node {
+        DirectoryNode directory = 1;
+        FileNode file = 2;
+        SymlinkNode symlink = 3;
+    }
+}
+
+// Nix C++ uses NAR (Nix Archive) as a format to transfer store paths,
+// and stores metadata and signatures in NARInfo files.
+// Store all these attributes in a separate message.
+//
+// This is useful to render .narinfo files to clients, or to preserve/validate
+// these signatures.
+// As verifying these signatures requires the whole NAR file to be synthesized,
+// moving to another signature scheme is desired.
+// Even then, it still makes sense to hold this data, for old clients.
+message NARInfo {
+    // This represents a (parsed) signature line in a .narinfo file.
+    message Signature {
+        string name = 1;
+        bytes data = 2;
+    };
+
+    // This size of the NAR file, in bytes.
+    uint64 nar_size = 1;
+
+    // The sha256 of the NAR file representation.
+    bytes nar_sha256 = 2;
+
+    // The signatures in a .narinfo file.
+    repeated Signature signatures = 3;
+
+    // A list of references. To validate .narinfo signatures, a fingerprint
+    // needs to be constructed.
+    // This fingerprint doesn't just contain the hashes of the output paths of
+    // all references (like PathInfo.references), but their whole (base)names,
+    // so we need to keep them somewhere.
+    repeated string reference_names = 4;
+
+}
diff --git a/tvix/store/protos/rpc_blobstore.pb.go b/tvix/store/protos/rpc_blobstore.pb.go
new file mode 100644
index 000000000000..850716757668
--- /dev/null
+++ b/tvix/store/protos/rpc_blobstore.pb.go
@@ -0,0 +1,542 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.29.1
+// 	protoc        (unknown)
+// source: tvix/store/protos/rpc_blobstore.proto
+
+package storev1
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type StatBlobRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The blake3 digest of the blob requested
+	Digest []byte `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"`
+	// Whether to include the chunks field
+	IncludeChunks bool `protobuf:"varint,2,opt,name=include_chunks,json=includeChunks,proto3" json:"include_chunks,omitempty"`
+	// Whether to include the inline_bao field, containing an (outboard) bao.
+	// The [bao](https://github.com/oconnor663/bao/blob/master/docs/spec.md)
+	// can be used to validate chunks end up hashing to the same root digest.
+	// These only really matter when only downloading parts of a blob. Some
+	// caution needs to be applied when validating chunks - the bao works with
+	// 1K leaf nodes, which might not align with the chunk sizes - this might
+	// imply a neighboring chunk might need to be (partially) fetched to
+	// validate the hash.
+	IncludeBao bool `protobuf:"varint,3,opt,name=include_bao,json=includeBao,proto3" json:"include_bao,omitempty"`
+}
+
+func (x *StatBlobRequest) Reset() {
+	*x = StatBlobRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *StatBlobRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*StatBlobRequest) ProtoMessage() {}
+
+func (x *StatBlobRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use StatBlobRequest.ProtoReflect.Descriptor instead.
+func (*StatBlobRequest) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_blobstore_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *StatBlobRequest) GetDigest() []byte {
+	if x != nil {
+		return x.Digest
+	}
+	return nil
+}
+
+func (x *StatBlobRequest) GetIncludeChunks() bool {
+	if x != nil {
+		return x.IncludeChunks
+	}
+	return false
+}
+
+func (x *StatBlobRequest) GetIncludeBao() bool {
+	if x != nil {
+		return x.IncludeBao
+	}
+	return false
+}
+
+// BlobMeta provides more granular chunking information for the requested blob,
+// and baos.
+type BlobMeta struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// This provides a list of chunks.
+	// Concatenating their contents would produce a blob with the digest that
+	// was specified in the request.
+	Chunks    []*BlobMeta_ChunkMeta `protobuf:"bytes,1,rep,name=chunks,proto3" json:"chunks,omitempty"`
+	InlineBao []byte                `protobuf:"bytes,2,opt,name=inline_bao,json=inlineBao,proto3" json:"inline_bao,omitempty"`
+}
+
+func (x *BlobMeta) Reset() {
+	*x = BlobMeta{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *BlobMeta) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BlobMeta) ProtoMessage() {}
+
+func (x *BlobMeta) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use BlobMeta.ProtoReflect.Descriptor instead.
+func (*BlobMeta) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_blobstore_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *BlobMeta) GetChunks() []*BlobMeta_ChunkMeta {
+	if x != nil {
+		return x.Chunks
+	}
+	return nil
+}
+
+func (x *BlobMeta) GetInlineBao() []byte {
+	if x != nil {
+		return x.InlineBao
+	}
+	return nil
+}
+
+type ReadBlobRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The blake3 digest of the blob or chunk requested
+	Digest []byte `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"`
+}
+
+func (x *ReadBlobRequest) Reset() {
+	*x = ReadBlobRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *ReadBlobRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ReadBlobRequest) ProtoMessage() {}
+
+func (x *ReadBlobRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use ReadBlobRequest.ProtoReflect.Descriptor instead.
+func (*ReadBlobRequest) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_blobstore_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *ReadBlobRequest) GetDigest() []byte {
+	if x != nil {
+		return x.Digest
+	}
+	return nil
+}
+
+// This represents some bytes of a blob.
+// Blobs are sent in smaller chunks to keep message sizes manageable.
+type BlobChunk struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
+}
+
+func (x *BlobChunk) Reset() {
+	*x = BlobChunk{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *BlobChunk) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BlobChunk) ProtoMessage() {}
+
+func (x *BlobChunk) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[3]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use BlobChunk.ProtoReflect.Descriptor instead.
+func (*BlobChunk) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_blobstore_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *BlobChunk) GetData() []byte {
+	if x != nil {
+		return x.Data
+	}
+	return nil
+}
+
+type PutBlobResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The blake3 digest of the data that was sent.
+	Digest []byte `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"`
+}
+
+func (x *PutBlobResponse) Reset() {
+	*x = PutBlobResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PutBlobResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PutBlobResponse) ProtoMessage() {}
+
+func (x *PutBlobResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[4]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PutBlobResponse.ProtoReflect.Descriptor instead.
+func (*PutBlobResponse) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_blobstore_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *PutBlobResponse) GetDigest() []byte {
+	if x != nil {
+		return x.Digest
+	}
+	return nil
+}
+
+type BlobMeta_ChunkMeta struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Digest []byte `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"`
+	Size   uint32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"`
+}
+
+func (x *BlobMeta_ChunkMeta) Reset() {
+	*x = BlobMeta_ChunkMeta{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[5]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *BlobMeta_ChunkMeta) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BlobMeta_ChunkMeta) ProtoMessage() {}
+
+func (x *BlobMeta_ChunkMeta) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_blobstore_proto_msgTypes[5]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use BlobMeta_ChunkMeta.ProtoReflect.Descriptor instead.
+func (*BlobMeta_ChunkMeta) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_blobstore_proto_rawDescGZIP(), []int{1, 0}
+}
+
+func (x *BlobMeta_ChunkMeta) GetDigest() []byte {
+	if x != nil {
+		return x.Digest
+	}
+	return nil
+}
+
+func (x *BlobMeta_ChunkMeta) GetSize() uint32 {
+	if x != nil {
+		return x.Size
+	}
+	return 0
+}
+
+var File_tvix_store_protos_rpc_blobstore_proto protoreflect.FileDescriptor
+
+var file_tvix_store_protos_rpc_blobstore_proto_rawDesc = []byte{
+	0x0a, 0x25, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x73, 0x2f, 0x72, 0x70, 0x63, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x73, 0x74, 0x6f, 0x72,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74,
+	0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x22, 0x71, 0x0a, 0x0f, 0x53, 0x74, 0x61, 0x74, 0x42, 0x6c,
+	0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67,
+	0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73,
+	0x74, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x63, 0x68, 0x75,
+	0x6e, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x69, 0x6e, 0x63, 0x6c, 0x75,
+	0x64, 0x65, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x63, 0x6c,
+	0x75, 0x64, 0x65, 0x5f, 0x62, 0x61, 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69,
+	0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x42, 0x61, 0x6f, 0x22, 0x9d, 0x01, 0x0a, 0x08, 0x42, 0x6c,
+	0x6f, 0x62, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x39, 0x0a, 0x06, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x73,
+	0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74,
+	0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x4d, 0x65, 0x74, 0x61, 0x2e,
+	0x43, 0x68, 0x75, 0x6e, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x06, 0x63, 0x68, 0x75, 0x6e, 0x6b,
+	0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x62, 0x61, 0x6f, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x69, 0x6e, 0x6c, 0x69, 0x6e, 0x65, 0x42, 0x61, 0x6f,
+	0x1a, 0x37, 0x0a, 0x09, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x16, 0x0a,
+	0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64,
+	0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20,
+	0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x29, 0x0a, 0x0f, 0x52, 0x65, 0x61,
+	0x64, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06,
+	0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, 0x69,
+	0x67, 0x65, 0x73, 0x74, 0x22, 0x1f, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x43, 0x68, 0x75, 0x6e,
+	0x6b, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52,
+	0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x29, 0x0a, 0x0f, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65,
+	0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74,
+	0x32, 0xd5, 0x01, 0x0a, 0x0b, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x12, 0x3f, 0x0a, 0x04, 0x53, 0x74, 0x61, 0x74, 0x12, 0x1e, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e,
+	0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x42, 0x6c, 0x6f,
+	0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e,
+	0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x4d, 0x65, 0x74,
+	0x61, 0x12, 0x42, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x1e, 0x2e, 0x74, 0x76, 0x69, 0x78,
+	0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x42, 0x6c,
+	0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x76, 0x69, 0x78,
+	0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x43, 0x68,
+	0x75, 0x6e, 0x6b, 0x30, 0x01, 0x12, 0x41, 0x0a, 0x03, 0x50, 0x75, 0x74, 0x12, 0x18, 0x2e, 0x74,
+	0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x6c, 0x6f,
+	0x62, 0x43, 0x68, 0x75, 0x6e, 0x6b, 0x1a, 0x1e, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74,
+	0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x42, 0x28, 0x5a, 0x26, 0x63, 0x6f, 0x64, 0x65,
+	0x2e, 0x74, 0x76, 0x6c, 0x2e, 0x66, 0x79, 0x69, 0x2f, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74,
+	0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65,
+	0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_tvix_store_protos_rpc_blobstore_proto_rawDescOnce sync.Once
+	file_tvix_store_protos_rpc_blobstore_proto_rawDescData = file_tvix_store_protos_rpc_blobstore_proto_rawDesc
+)
+
+func file_tvix_store_protos_rpc_blobstore_proto_rawDescGZIP() []byte {
+	file_tvix_store_protos_rpc_blobstore_proto_rawDescOnce.Do(func() {
+		file_tvix_store_protos_rpc_blobstore_proto_rawDescData = protoimpl.X.CompressGZIP(file_tvix_store_protos_rpc_blobstore_proto_rawDescData)
+	})
+	return file_tvix_store_protos_rpc_blobstore_proto_rawDescData
+}
+
+var file_tvix_store_protos_rpc_blobstore_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
+var file_tvix_store_protos_rpc_blobstore_proto_goTypes = []interface{}{
+	(*StatBlobRequest)(nil),    // 0: tvix.store.v1.StatBlobRequest
+	(*BlobMeta)(nil),           // 1: tvix.store.v1.BlobMeta
+	(*ReadBlobRequest)(nil),    // 2: tvix.store.v1.ReadBlobRequest
+	(*BlobChunk)(nil),          // 3: tvix.store.v1.BlobChunk
+	(*PutBlobResponse)(nil),    // 4: tvix.store.v1.PutBlobResponse
+	(*BlobMeta_ChunkMeta)(nil), // 5: tvix.store.v1.BlobMeta.ChunkMeta
+}
+var file_tvix_store_protos_rpc_blobstore_proto_depIdxs = []int32{
+	5, // 0: tvix.store.v1.BlobMeta.chunks:type_name -> tvix.store.v1.BlobMeta.ChunkMeta
+	0, // 1: tvix.store.v1.BlobService.Stat:input_type -> tvix.store.v1.StatBlobRequest
+	2, // 2: tvix.store.v1.BlobService.Read:input_type -> tvix.store.v1.ReadBlobRequest
+	3, // 3: tvix.store.v1.BlobService.Put:input_type -> tvix.store.v1.BlobChunk
+	1, // 4: tvix.store.v1.BlobService.Stat:output_type -> tvix.store.v1.BlobMeta
+	3, // 5: tvix.store.v1.BlobService.Read:output_type -> tvix.store.v1.BlobChunk
+	4, // 6: tvix.store.v1.BlobService.Put:output_type -> tvix.store.v1.PutBlobResponse
+	4, // [4:7] is the sub-list for method output_type
+	1, // [1:4] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_tvix_store_protos_rpc_blobstore_proto_init() }
+func file_tvix_store_protos_rpc_blobstore_proto_init() {
+	if File_tvix_store_protos_rpc_blobstore_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_tvix_store_protos_rpc_blobstore_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*StatBlobRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_rpc_blobstore_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*BlobMeta); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_rpc_blobstore_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*ReadBlobRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_rpc_blobstore_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*BlobChunk); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_rpc_blobstore_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PutBlobResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_rpc_blobstore_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*BlobMeta_ChunkMeta); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_tvix_store_protos_rpc_blobstore_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   6,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_tvix_store_protos_rpc_blobstore_proto_goTypes,
+		DependencyIndexes: file_tvix_store_protos_rpc_blobstore_proto_depIdxs,
+		MessageInfos:      file_tvix_store_protos_rpc_blobstore_proto_msgTypes,
+	}.Build()
+	File_tvix_store_protos_rpc_blobstore_proto = out.File
+	file_tvix_store_protos_rpc_blobstore_proto_rawDesc = nil
+	file_tvix_store_protos_rpc_blobstore_proto_goTypes = nil
+	file_tvix_store_protos_rpc_blobstore_proto_depIdxs = nil
+}
diff --git a/tvix/store/protos/rpc_blobstore.proto b/tvix/store/protos/rpc_blobstore.proto
new file mode 100644
index 000000000000..a7d48cf7709c
--- /dev/null
+++ b/tvix/store/protos/rpc_blobstore.proto
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+syntax = "proto3";
+
+package tvix.store.v1;
+
+option go_package = "code.tvl.fyi/tvix/store/protos;storev1";
+
+service BlobService {
+    // Stat exposes metadata about a given blob,
+    // such as more granular chunking, baos.
+    // It implicitly allows checking for existence too, as asking this for a
+    // non-existing Blob will return a Status::not_found grpc error.
+    // If there's no more granular chunking available, the response will simply
+    // contain a single chunk.
+    rpc Stat(StatBlobRequest) returns (BlobMeta);
+
+    // Read returns a stream of BlobChunk, which is just a stream of bytes with
+    // the digest specified in ReadBlobRequest.
+    //
+    // The server may decide on whatever chunking it may seem fit as a size for
+    // the individual BlobChunk sent in the response stream.
+    //
+    rpc Read(ReadBlobRequest) returns (stream BlobChunk);
+
+    // Put uploads a Blob, by reading a stream of bytes.
+    //
+    // The way the data is chunked up in individual BlobChunk messages sent in
+    // the stream has no effect on how the server ends up chunking blobs up.
+    rpc Put(stream BlobChunk) returns (PutBlobResponse);
+}
+
+message StatBlobRequest {
+    // The blake3 digest of the blob requested
+    bytes digest = 1;
+
+    // Whether to include the chunks field
+    bool include_chunks = 2;
+    // Whether to include the inline_bao field, containing an (outboard) bao.
+    // The [bao](https://github.com/oconnor663/bao/blob/master/docs/spec.md)
+    // can be used to validate chunks end up hashing to the same root digest.
+    // These only really matter when only downloading parts of a blob. Some
+    // caution needs to be applied when validating chunks - the bao works with
+    // 1K leaf nodes, which might not align with the chunk sizes - this might
+    // imply a neighboring chunk might need to be (partially) fetched to
+    // validate the hash.
+    bool include_bao = 3;
+}
+
+// BlobMeta provides more granular chunking information for the requested blob,
+// and baos.
+message BlobMeta {
+    // This provides a list of chunks.
+    // Concatenating their contents would produce a blob with the digest that
+    // was specified in the request.
+    repeated ChunkMeta chunks = 1;
+
+    message ChunkMeta {
+        bytes digest = 1;
+        uint32 size = 2;
+    }
+
+    bytes inline_bao = 2;
+}
+
+message ReadBlobRequest {
+    // The blake3 digest of the blob or chunk requested
+    bytes digest = 1;
+}
+
+// This represents some bytes of a blob.
+// Blobs are sent in smaller chunks to keep message sizes manageable.
+message BlobChunk {
+    bytes data = 1;
+}
+
+message PutBlobResponse {
+    // The blake3 digest of the data that was sent.
+    bytes digest = 1;
+}
diff --git a/tvix/store/protos/rpc_blobstore_grpc.pb.go b/tvix/store/protos/rpc_blobstore_grpc.pb.go
new file mode 100644
index 000000000000..3842ea288544
--- /dev/null
+++ b/tvix/store/protos/rpc_blobstore_grpc.pb.go
@@ -0,0 +1,278 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc             (unknown)
+// source: tvix/store/protos/rpc_blobstore.proto
+
+package storev1
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+	BlobService_Stat_FullMethodName = "/tvix.store.v1.BlobService/Stat"
+	BlobService_Read_FullMethodName = "/tvix.store.v1.BlobService/Read"
+	BlobService_Put_FullMethodName  = "/tvix.store.v1.BlobService/Put"
+)
+
+// BlobServiceClient is the client API for BlobService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type BlobServiceClient interface {
+	// Stat exposes metadata about a given blob,
+	// such as more granular chunking, baos.
+	// It implicitly allows checking for existence too, as asking this for a
+	// non-existing Blob will return a Status::not_found grpc error.
+	// If there's no more granular chunking available, the response will simply
+	// contain a single chunk.
+	Stat(ctx context.Context, in *StatBlobRequest, opts ...grpc.CallOption) (*BlobMeta, error)
+	// Read returns a stream of BlobChunk, which is just a stream of bytes with
+	// the digest specified in ReadBlobRequest.
+	//
+	// The server may decide on whatever chunking it may seem fit as a size for
+	// the individual BlobChunk sent in the response stream.
+	Read(ctx context.Context, in *ReadBlobRequest, opts ...grpc.CallOption) (BlobService_ReadClient, error)
+	// Put uploads a Blob, by reading a stream of bytes.
+	//
+	// The way the data is chunked up in individual BlobChunk messages sent in
+	// the stream has no effect on how the server ends up chunking blobs up.
+	Put(ctx context.Context, opts ...grpc.CallOption) (BlobService_PutClient, error)
+}
+
+type blobServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewBlobServiceClient(cc grpc.ClientConnInterface) BlobServiceClient {
+	return &blobServiceClient{cc}
+}
+
+func (c *blobServiceClient) Stat(ctx context.Context, in *StatBlobRequest, opts ...grpc.CallOption) (*BlobMeta, error) {
+	out := new(BlobMeta)
+	err := c.cc.Invoke(ctx, BlobService_Stat_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *blobServiceClient) Read(ctx context.Context, in *ReadBlobRequest, opts ...grpc.CallOption) (BlobService_ReadClient, error) {
+	stream, err := c.cc.NewStream(ctx, &BlobService_ServiceDesc.Streams[0], BlobService_Read_FullMethodName, opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &blobServiceReadClient{stream}
+	if err := x.ClientStream.SendMsg(in); err != nil {
+		return nil, err
+	}
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	return x, nil
+}
+
+type BlobService_ReadClient interface {
+	Recv() (*BlobChunk, error)
+	grpc.ClientStream
+}
+
+type blobServiceReadClient struct {
+	grpc.ClientStream
+}
+
+func (x *blobServiceReadClient) Recv() (*BlobChunk, error) {
+	m := new(BlobChunk)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+func (c *blobServiceClient) Put(ctx context.Context, opts ...grpc.CallOption) (BlobService_PutClient, error) {
+	stream, err := c.cc.NewStream(ctx, &BlobService_ServiceDesc.Streams[1], BlobService_Put_FullMethodName, opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &blobServicePutClient{stream}
+	return x, nil
+}
+
+type BlobService_PutClient interface {
+	Send(*BlobChunk) error
+	CloseAndRecv() (*PutBlobResponse, error)
+	grpc.ClientStream
+}
+
+type blobServicePutClient struct {
+	grpc.ClientStream
+}
+
+func (x *blobServicePutClient) Send(m *BlobChunk) error {
+	return x.ClientStream.SendMsg(m)
+}
+
+func (x *blobServicePutClient) CloseAndRecv() (*PutBlobResponse, error) {
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	m := new(PutBlobResponse)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// BlobServiceServer is the server API for BlobService service.
+// All implementations must embed UnimplementedBlobServiceServer
+// for forward compatibility
+type BlobServiceServer interface {
+	// Stat exposes metadata about a given blob,
+	// such as more granular chunking, baos.
+	// It implicitly allows checking for existence too, as asking this for a
+	// non-existing Blob will return a Status::not_found grpc error.
+	// If there's no more granular chunking available, the response will simply
+	// contain a single chunk.
+	Stat(context.Context, *StatBlobRequest) (*BlobMeta, error)
+	// Read returns a stream of BlobChunk, which is just a stream of bytes with
+	// the digest specified in ReadBlobRequest.
+	//
+	// The server may decide on whatever chunking it may seem fit as a size for
+	// the individual BlobChunk sent in the response stream.
+	Read(*ReadBlobRequest, BlobService_ReadServer) error
+	// Put uploads a Blob, by reading a stream of bytes.
+	//
+	// The way the data is chunked up in individual BlobChunk messages sent in
+	// the stream has no effect on how the server ends up chunking blobs up.
+	Put(BlobService_PutServer) error
+	mustEmbedUnimplementedBlobServiceServer()
+}
+
+// UnimplementedBlobServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedBlobServiceServer struct {
+}
+
+func (UnimplementedBlobServiceServer) Stat(context.Context, *StatBlobRequest) (*BlobMeta, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method Stat not implemented")
+}
+func (UnimplementedBlobServiceServer) Read(*ReadBlobRequest, BlobService_ReadServer) error {
+	return status.Errorf(codes.Unimplemented, "method Read not implemented")
+}
+func (UnimplementedBlobServiceServer) Put(BlobService_PutServer) error {
+	return status.Errorf(codes.Unimplemented, "method Put not implemented")
+}
+func (UnimplementedBlobServiceServer) mustEmbedUnimplementedBlobServiceServer() {}
+
+// UnsafeBlobServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to BlobServiceServer will
+// result in compilation errors.
+type UnsafeBlobServiceServer interface {
+	mustEmbedUnimplementedBlobServiceServer()
+}
+
+func RegisterBlobServiceServer(s grpc.ServiceRegistrar, srv BlobServiceServer) {
+	s.RegisterService(&BlobService_ServiceDesc, srv)
+}
+
+func _BlobService_Stat_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(StatBlobRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(BlobServiceServer).Stat(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: BlobService_Stat_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(BlobServiceServer).Stat(ctx, req.(*StatBlobRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _BlobService_Read_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(ReadBlobRequest)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(BlobServiceServer).Read(m, &blobServiceReadServer{stream})
+}
+
+type BlobService_ReadServer interface {
+	Send(*BlobChunk) error
+	grpc.ServerStream
+}
+
+type blobServiceReadServer struct {
+	grpc.ServerStream
+}
+
+func (x *blobServiceReadServer) Send(m *BlobChunk) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func _BlobService_Put_Handler(srv interface{}, stream grpc.ServerStream) error {
+	return srv.(BlobServiceServer).Put(&blobServicePutServer{stream})
+}
+
+type BlobService_PutServer interface {
+	SendAndClose(*PutBlobResponse) error
+	Recv() (*BlobChunk, error)
+	grpc.ServerStream
+}
+
+type blobServicePutServer struct {
+	grpc.ServerStream
+}
+
+func (x *blobServicePutServer) SendAndClose(m *PutBlobResponse) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func (x *blobServicePutServer) Recv() (*BlobChunk, error) {
+	m := new(BlobChunk)
+	if err := x.ServerStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// BlobService_ServiceDesc is the grpc.ServiceDesc for BlobService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var BlobService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "tvix.store.v1.BlobService",
+	HandlerType: (*BlobServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "Stat",
+			Handler:    _BlobService_Stat_Handler,
+		},
+	},
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "Read",
+			Handler:       _BlobService_Read_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "Put",
+			Handler:       _BlobService_Put_Handler,
+			ClientStreams: true,
+		},
+	},
+	Metadata: "tvix/store/protos/rpc_blobstore.proto",
+}
diff --git a/tvix/store/protos/rpc_directory.pb.go b/tvix/store/protos/rpc_directory.pb.go
new file mode 100644
index 000000000000..5a1cdccca081
--- /dev/null
+++ b/tvix/store/protos/rpc_directory.pb.go
@@ -0,0 +1,271 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.29.1
+// 	protoc        (unknown)
+// source: tvix/store/protos/rpc_directory.proto
+
+package storev1
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type GetDirectoryRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Types that are assignable to ByWhat:
+	//
+	//	*GetDirectoryRequest_Digest
+	ByWhat isGetDirectoryRequest_ByWhat `protobuf_oneof:"by_what"`
+	// If set to true, recursively resolve all child Directory messages.
+	// Directory messages SHOULD be streamed in a recursive breadth-first walk,
+	// but other orders are also fine, as long as Directory messages are only
+	// sent after they are referred to from previously sent Directory messages.
+	Recursive bool `protobuf:"varint,2,opt,name=recursive,proto3" json:"recursive,omitempty"`
+}
+
+func (x *GetDirectoryRequest) Reset() {
+	*x = GetDirectoryRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_directory_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetDirectoryRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetDirectoryRequest) ProtoMessage() {}
+
+func (x *GetDirectoryRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_directory_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetDirectoryRequest.ProtoReflect.Descriptor instead.
+func (*GetDirectoryRequest) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_directory_proto_rawDescGZIP(), []int{0}
+}
+
+func (m *GetDirectoryRequest) GetByWhat() isGetDirectoryRequest_ByWhat {
+	if m != nil {
+		return m.ByWhat
+	}
+	return nil
+}
+
+func (x *GetDirectoryRequest) GetDigest() []byte {
+	if x, ok := x.GetByWhat().(*GetDirectoryRequest_Digest); ok {
+		return x.Digest
+	}
+	return nil
+}
+
+func (x *GetDirectoryRequest) GetRecursive() bool {
+	if x != nil {
+		return x.Recursive
+	}
+	return false
+}
+
+type isGetDirectoryRequest_ByWhat interface {
+	isGetDirectoryRequest_ByWhat()
+}
+
+type GetDirectoryRequest_Digest struct {
+	// The blake3 hash of the (root) Directory message, serialized in
+	// protobuf canonical form.
+	// Keep in mind this can be a subtree of another root.
+	Digest []byte `protobuf:"bytes,1,opt,name=digest,proto3,oneof"`
+}
+
+func (*GetDirectoryRequest_Digest) isGetDirectoryRequest_ByWhat() {}
+
+type PutDirectoryResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	RootDigest []byte `protobuf:"bytes,1,opt,name=root_digest,json=rootDigest,proto3" json:"root_digest,omitempty"`
+}
+
+func (x *PutDirectoryResponse) Reset() {
+	*x = PutDirectoryResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_directory_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *PutDirectoryResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*PutDirectoryResponse) ProtoMessage() {}
+
+func (x *PutDirectoryResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_directory_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use PutDirectoryResponse.ProtoReflect.Descriptor instead.
+func (*PutDirectoryResponse) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_directory_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *PutDirectoryResponse) GetRootDigest() []byte {
+	if x != nil {
+		return x.RootDigest
+	}
+	return nil
+}
+
+var File_tvix_store_protos_rpc_directory_proto protoreflect.FileDescriptor
+
+var file_tvix_store_protos_rpc_directory_proto_rawDesc = []byte{
+	0x0a, 0x25, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x73, 0x2f, 0x72, 0x70, 0x63, 0x5f, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72,
+	0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74,
+	0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f,
+	0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x61, 0x73, 0x74, 0x6f, 0x72,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x58, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x69,
+	0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18,
+	0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00,
+	0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x75,
+	0x72, 0x73, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, 0x65, 0x63,
+	0x75, 0x72, 0x73, 0x69, 0x76, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x62, 0x79, 0x5f, 0x77, 0x68, 0x61,
+	0x74, 0x22, 0x37, 0x0a, 0x14, 0x50, 0x75, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72,
+	0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x6f, 0x6f,
+	0x74, 0x5f, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a,
+	0x72, 0x6f, 0x6f, 0x74, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x32, 0xa1, 0x01, 0x0a, 0x10, 0x44,
+	0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12,
+	0x45, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x22, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74,
+	0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74,
+	0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x76, 0x69,
+	0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x72, 0x65, 0x63,
+	0x74, 0x6f, 0x72, 0x79, 0x30, 0x01, 0x12, 0x46, 0x0a, 0x03, 0x50, 0x75, 0x74, 0x12, 0x18, 0x2e,
+	0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69,
+	0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x23, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73,
+	0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63,
+	0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x42, 0x28,
+	0x5a, 0x26, 0x63, 0x6f, 0x64, 0x65, 0x2e, 0x74, 0x76, 0x6c, 0x2e, 0x66, 0x79, 0x69, 0x2f, 0x74,
+	0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73,
+	0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_tvix_store_protos_rpc_directory_proto_rawDescOnce sync.Once
+	file_tvix_store_protos_rpc_directory_proto_rawDescData = file_tvix_store_protos_rpc_directory_proto_rawDesc
+)
+
+func file_tvix_store_protos_rpc_directory_proto_rawDescGZIP() []byte {
+	file_tvix_store_protos_rpc_directory_proto_rawDescOnce.Do(func() {
+		file_tvix_store_protos_rpc_directory_proto_rawDescData = protoimpl.X.CompressGZIP(file_tvix_store_protos_rpc_directory_proto_rawDescData)
+	})
+	return file_tvix_store_protos_rpc_directory_proto_rawDescData
+}
+
+var file_tvix_store_protos_rpc_directory_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_tvix_store_protos_rpc_directory_proto_goTypes = []interface{}{
+	(*GetDirectoryRequest)(nil),  // 0: tvix.store.v1.GetDirectoryRequest
+	(*PutDirectoryResponse)(nil), // 1: tvix.store.v1.PutDirectoryResponse
+	(*Directory)(nil),            // 2: tvix.store.v1.Directory
+}
+var file_tvix_store_protos_rpc_directory_proto_depIdxs = []int32{
+	0, // 0: tvix.store.v1.DirectoryService.Get:input_type -> tvix.store.v1.GetDirectoryRequest
+	2, // 1: tvix.store.v1.DirectoryService.Put:input_type -> tvix.store.v1.Directory
+	2, // 2: tvix.store.v1.DirectoryService.Get:output_type -> tvix.store.v1.Directory
+	1, // 3: tvix.store.v1.DirectoryService.Put:output_type -> tvix.store.v1.PutDirectoryResponse
+	2, // [2:4] is the sub-list for method output_type
+	0, // [0:2] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_tvix_store_protos_rpc_directory_proto_init() }
+func file_tvix_store_protos_rpc_directory_proto_init() {
+	if File_tvix_store_protos_rpc_directory_proto != nil {
+		return
+	}
+	file_tvix_store_protos_castore_proto_init()
+	if !protoimpl.UnsafeEnabled {
+		file_tvix_store_protos_rpc_directory_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetDirectoryRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_rpc_directory_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*PutDirectoryResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	file_tvix_store_protos_rpc_directory_proto_msgTypes[0].OneofWrappers = []interface{}{
+		(*GetDirectoryRequest_Digest)(nil),
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_tvix_store_protos_rpc_directory_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_tvix_store_protos_rpc_directory_proto_goTypes,
+		DependencyIndexes: file_tvix_store_protos_rpc_directory_proto_depIdxs,
+		MessageInfos:      file_tvix_store_protos_rpc_directory_proto_msgTypes,
+	}.Build()
+	File_tvix_store_protos_rpc_directory_proto = out.File
+	file_tvix_store_protos_rpc_directory_proto_rawDesc = nil
+	file_tvix_store_protos_rpc_directory_proto_goTypes = nil
+	file_tvix_store_protos_rpc_directory_proto_depIdxs = nil
+}
diff --git a/tvix/store/protos/rpc_directory.proto b/tvix/store/protos/rpc_directory.proto
new file mode 100644
index 000000000000..0aeed5c3c0e1
--- /dev/null
+++ b/tvix/store/protos/rpc_directory.proto
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+syntax = "proto3";
+
+package tvix.store.v1;
+
+import "tvix/store/protos/castore.proto";
+
+option go_package = "code.tvl.fyi/tvix/store/protos;storev1";
+
+service DirectoryService {
+  // Get retrieves a stream of Directory messages, by using the lookup
+  // parameters in GetDirectoryRequest.
+  // Keep in mind multiple DirectoryNodes in different parts of the graph might
+  // have the same digest if they have the same underlying contents,
+  // so sending subsequent ones can be omitted.
+  rpc Get(GetDirectoryRequest) returns (stream Directory);
+
+  // Put uploads a graph of Directory messages.
+  // Individual Directory messages need to be send in an order walking up
+  // from the leaves to the root - a Directory message can only refer to
+  // Directory messages previously sent in the same stream.
+  // Keep in mind multiple DirectoryNodes in different parts of the graph might
+  // have the same digest if they have the same underlying contents,
+  // so sending subsequent ones can be omitted.
+  // We might add a separate method, allowing to send partial graphs at a later
+  // time, if requiring to send the full graph turns out to be a problem.
+  rpc Put(stream Directory) returns (PutDirectoryResponse);
+}
+
+message GetDirectoryRequest {
+  oneof by_what {
+      // The blake3 hash of the (root) Directory message, serialized in
+      // protobuf canonical form.
+      // Keep in mind this can be a subtree of another root.
+      bytes digest = 1;
+  }
+
+  // If set to true, recursively resolve all child Directory messages.
+  // Directory messages SHOULD be streamed in a recursive breadth-first walk,
+  // but other orders are also fine, as long as Directory messages are only
+  // sent after they are referred to from previously sent Directory messages.
+  bool recursive = 2;
+}
+
+message PutDirectoryResponse {
+  bytes root_digest = 1;
+}
diff --git a/tvix/store/protos/rpc_directory_grpc.pb.go b/tvix/store/protos/rpc_directory_grpc.pb.go
new file mode 100644
index 000000000000..a578dbd89d39
--- /dev/null
+++ b/tvix/store/protos/rpc_directory_grpc.pb.go
@@ -0,0 +1,238 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc             (unknown)
+// source: tvix/store/protos/rpc_directory.proto
+
+package storev1
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+	DirectoryService_Get_FullMethodName = "/tvix.store.v1.DirectoryService/Get"
+	DirectoryService_Put_FullMethodName = "/tvix.store.v1.DirectoryService/Put"
+)
+
+// DirectoryServiceClient is the client API for DirectoryService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type DirectoryServiceClient interface {
+	// Get retrieves a stream of Directory messages, by using the lookup
+	// parameters in GetDirectoryRequest.
+	// Keep in mind multiple DirectoryNodes in different parts of the graph might
+	// have the same digest if they have the same underlying contents,
+	// so sending subsequent ones can be omitted.
+	Get(ctx context.Context, in *GetDirectoryRequest, opts ...grpc.CallOption) (DirectoryService_GetClient, error)
+	// Put uploads a graph of Directory messages.
+	// Individual Directory messages need to be send in an order walking up
+	// from the leaves to the root - a Directory message can only refer to
+	// Directory messages previously sent in the same stream.
+	// Keep in mind multiple DirectoryNodes in different parts of the graph might
+	// have the same digest if they have the same underlying contents,
+	// so sending subsequent ones can be omitted.
+	// We might add a separate method, allowing to send partial graphs at a later
+	// time, if requiring to send the full graph turns out to be a problem.
+	Put(ctx context.Context, opts ...grpc.CallOption) (DirectoryService_PutClient, error)
+}
+
+type directoryServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewDirectoryServiceClient(cc grpc.ClientConnInterface) DirectoryServiceClient {
+	return &directoryServiceClient{cc}
+}
+
+func (c *directoryServiceClient) Get(ctx context.Context, in *GetDirectoryRequest, opts ...grpc.CallOption) (DirectoryService_GetClient, error) {
+	stream, err := c.cc.NewStream(ctx, &DirectoryService_ServiceDesc.Streams[0], DirectoryService_Get_FullMethodName, opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &directoryServiceGetClient{stream}
+	if err := x.ClientStream.SendMsg(in); err != nil {
+		return nil, err
+	}
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	return x, nil
+}
+
+type DirectoryService_GetClient interface {
+	Recv() (*Directory, error)
+	grpc.ClientStream
+}
+
+type directoryServiceGetClient struct {
+	grpc.ClientStream
+}
+
+func (x *directoryServiceGetClient) Recv() (*Directory, error) {
+	m := new(Directory)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+func (c *directoryServiceClient) Put(ctx context.Context, opts ...grpc.CallOption) (DirectoryService_PutClient, error) {
+	stream, err := c.cc.NewStream(ctx, &DirectoryService_ServiceDesc.Streams[1], DirectoryService_Put_FullMethodName, opts...)
+	if err != nil {
+		return nil, err
+	}
+	x := &directoryServicePutClient{stream}
+	return x, nil
+}
+
+type DirectoryService_PutClient interface {
+	Send(*Directory) error
+	CloseAndRecv() (*PutDirectoryResponse, error)
+	grpc.ClientStream
+}
+
+type directoryServicePutClient struct {
+	grpc.ClientStream
+}
+
+func (x *directoryServicePutClient) Send(m *Directory) error {
+	return x.ClientStream.SendMsg(m)
+}
+
+func (x *directoryServicePutClient) CloseAndRecv() (*PutDirectoryResponse, error) {
+	if err := x.ClientStream.CloseSend(); err != nil {
+		return nil, err
+	}
+	m := new(PutDirectoryResponse)
+	if err := x.ClientStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// DirectoryServiceServer is the server API for DirectoryService service.
+// All implementations must embed UnimplementedDirectoryServiceServer
+// for forward compatibility
+type DirectoryServiceServer interface {
+	// Get retrieves a stream of Directory messages, by using the lookup
+	// parameters in GetDirectoryRequest.
+	// Keep in mind multiple DirectoryNodes in different parts of the graph might
+	// have the same digest if they have the same underlying contents,
+	// so sending subsequent ones can be omitted.
+	Get(*GetDirectoryRequest, DirectoryService_GetServer) error
+	// Put uploads a graph of Directory messages.
+	// Individual Directory messages need to be send in an order walking up
+	// from the leaves to the root - a Directory message can only refer to
+	// Directory messages previously sent in the same stream.
+	// Keep in mind multiple DirectoryNodes in different parts of the graph might
+	// have the same digest if they have the same underlying contents,
+	// so sending subsequent ones can be omitted.
+	// We might add a separate method, allowing to send partial graphs at a later
+	// time, if requiring to send the full graph turns out to be a problem.
+	Put(DirectoryService_PutServer) error
+	mustEmbedUnimplementedDirectoryServiceServer()
+}
+
+// UnimplementedDirectoryServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedDirectoryServiceServer struct {
+}
+
+func (UnimplementedDirectoryServiceServer) Get(*GetDirectoryRequest, DirectoryService_GetServer) error {
+	return status.Errorf(codes.Unimplemented, "method Get not implemented")
+}
+func (UnimplementedDirectoryServiceServer) Put(DirectoryService_PutServer) error {
+	return status.Errorf(codes.Unimplemented, "method Put not implemented")
+}
+func (UnimplementedDirectoryServiceServer) mustEmbedUnimplementedDirectoryServiceServer() {}
+
+// UnsafeDirectoryServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to DirectoryServiceServer will
+// result in compilation errors.
+type UnsafeDirectoryServiceServer interface {
+	mustEmbedUnimplementedDirectoryServiceServer()
+}
+
+func RegisterDirectoryServiceServer(s grpc.ServiceRegistrar, srv DirectoryServiceServer) {
+	s.RegisterService(&DirectoryService_ServiceDesc, srv)
+}
+
+func _DirectoryService_Get_Handler(srv interface{}, stream grpc.ServerStream) error {
+	m := new(GetDirectoryRequest)
+	if err := stream.RecvMsg(m); err != nil {
+		return err
+	}
+	return srv.(DirectoryServiceServer).Get(m, &directoryServiceGetServer{stream})
+}
+
+type DirectoryService_GetServer interface {
+	Send(*Directory) error
+	grpc.ServerStream
+}
+
+type directoryServiceGetServer struct {
+	grpc.ServerStream
+}
+
+func (x *directoryServiceGetServer) Send(m *Directory) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func _DirectoryService_Put_Handler(srv interface{}, stream grpc.ServerStream) error {
+	return srv.(DirectoryServiceServer).Put(&directoryServicePutServer{stream})
+}
+
+type DirectoryService_PutServer interface {
+	SendAndClose(*PutDirectoryResponse) error
+	Recv() (*Directory, error)
+	grpc.ServerStream
+}
+
+type directoryServicePutServer struct {
+	grpc.ServerStream
+}
+
+func (x *directoryServicePutServer) SendAndClose(m *PutDirectoryResponse) error {
+	return x.ServerStream.SendMsg(m)
+}
+
+func (x *directoryServicePutServer) Recv() (*Directory, error) {
+	m := new(Directory)
+	if err := x.ServerStream.RecvMsg(m); err != nil {
+		return nil, err
+	}
+	return m, nil
+}
+
+// DirectoryService_ServiceDesc is the grpc.ServiceDesc for DirectoryService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var DirectoryService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "tvix.store.v1.DirectoryService",
+	HandlerType: (*DirectoryServiceServer)(nil),
+	Methods:     []grpc.MethodDesc{},
+	Streams: []grpc.StreamDesc{
+		{
+			StreamName:    "Get",
+			Handler:       _DirectoryService_Get_Handler,
+			ServerStreams: true,
+		},
+		{
+			StreamName:    "Put",
+			Handler:       _DirectoryService_Put_Handler,
+			ClientStreams: true,
+		},
+	},
+	Metadata: "tvix/store/protos/rpc_directory.proto",
+}
diff --git a/tvix/store/protos/rpc_pathinfo.pb.go b/tvix/store/protos/rpc_pathinfo.pb.go
new file mode 100644
index 000000000000..bbbf3122ba23
--- /dev/null
+++ b/tvix/store/protos/rpc_pathinfo.pb.go
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.29.1
+// 	protoc        (unknown)
+// source: tvix/store/protos/rpc_pathinfo.proto
+
+package storev1
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// GetPathInfoRequest describes the lookup parameters that can be used to
+// lookup a PathInfo objects.
+// Currently, only a lookup by output hash is supported.
+type GetPathInfoRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// Types that are assignable to ByWhat:
+	//
+	//	*GetPathInfoRequest_ByOutputHash
+	ByWhat isGetPathInfoRequest_ByWhat `protobuf_oneof:"by_what"`
+}
+
+func (x *GetPathInfoRequest) Reset() {
+	*x = GetPathInfoRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_pathinfo_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *GetPathInfoRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetPathInfoRequest) ProtoMessage() {}
+
+func (x *GetPathInfoRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_pathinfo_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetPathInfoRequest.ProtoReflect.Descriptor instead.
+func (*GetPathInfoRequest) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_pathinfo_proto_rawDescGZIP(), []int{0}
+}
+
+func (m *GetPathInfoRequest) GetByWhat() isGetPathInfoRequest_ByWhat {
+	if m != nil {
+		return m.ByWhat
+	}
+	return nil
+}
+
+func (x *GetPathInfoRequest) GetByOutputHash() []byte {
+	if x, ok := x.GetByWhat().(*GetPathInfoRequest_ByOutputHash); ok {
+		return x.ByOutputHash
+	}
+	return nil
+}
+
+type isGetPathInfoRequest_ByWhat interface {
+	isGetPathInfoRequest_ByWhat()
+}
+
+type GetPathInfoRequest_ByOutputHash struct {
+	// The output hash of a nix path (20 bytes).
+	// This is the nixbase32-decoded portion of a Nix output path, so to substitute
+	// /nix/store/xm35nga2g20mz5sm5l6n8v3bdm86yj83-cowsay-3.04
+	// this field would contain nixbase32dec("xm35nga2g20mz5sm5l6n8v3bdm86yj83").
+	ByOutputHash []byte `protobuf:"bytes,1,opt,name=by_output_hash,json=byOutputHash,proto3,oneof"`
+}
+
+func (*GetPathInfoRequest_ByOutputHash) isGetPathInfoRequest_ByWhat() {}
+
+// CalculateNARResponse is the response returned by the CalculateNAR request.
+//
+// It contains the size of the NAR representation (in bytes), and the sha56
+// digest.
+type CalculateNARResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// This size of the NAR file, in bytes.
+	NarSize uint64 `protobuf:"varint,1,opt,name=nar_size,json=narSize,proto3" json:"nar_size,omitempty"`
+	// The sha256 of the NAR file representation.
+	NarSha256 []byte `protobuf:"bytes,2,opt,name=nar_sha256,json=narSha256,proto3" json:"nar_sha256,omitempty"`
+}
+
+func (x *CalculateNARResponse) Reset() {
+	*x = CalculateNARResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_tvix_store_protos_rpc_pathinfo_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CalculateNARResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CalculateNARResponse) ProtoMessage() {}
+
+func (x *CalculateNARResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_tvix_store_protos_rpc_pathinfo_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use CalculateNARResponse.ProtoReflect.Descriptor instead.
+func (*CalculateNARResponse) Descriptor() ([]byte, []int) {
+	return file_tvix_store_protos_rpc_pathinfo_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CalculateNARResponse) GetNarSize() uint64 {
+	if x != nil {
+		return x.NarSize
+	}
+	return 0
+}
+
+func (x *CalculateNARResponse) GetNarSha256() []byte {
+	if x != nil {
+		return x.NarSha256
+	}
+	return nil
+}
+
+var File_tvix_store_protos_rpc_pathinfo_proto protoreflect.FileDescriptor
+
+var file_tvix_store_protos_rpc_pathinfo_proto_rawDesc = []byte{
+	0x0a, 0x24, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x73, 0x2f, 0x72, 0x70, 0x63, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x69, 0x6e, 0x66, 0x6f,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f,
+	0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x20, 0x74, 0x76, 0x69, 0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72,
+	0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x70, 0x61, 0x74, 0x68, 0x69, 0x6e, 0x66,
+	0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x47, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x50, 0x61,
+	0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a,
+	0x0e, 0x62, 0x79, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x0c, 0x62, 0x79, 0x4f, 0x75, 0x74, 0x70, 0x75,
+	0x74, 0x48, 0x61, 0x73, 0x68, 0x42, 0x09, 0x0a, 0x07, 0x62, 0x79, 0x5f, 0x77, 0x68, 0x61, 0x74,
+	0x22, 0x50, 0x0a, 0x14, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x41, 0x52,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x61, 0x72, 0x5f,
+	0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x6e, 0x61, 0x72, 0x53,
+	0x69, 0x7a, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x61, 0x72, 0x5f, 0x73, 0x68, 0x61, 0x32, 0x35,
+	0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x6e, 0x61, 0x72, 0x53, 0x68, 0x61, 0x32,
+	0x35, 0x36, 0x32, 0xd7, 0x01, 0x0a, 0x0f, 0x50, 0x61, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x53,
+	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x41, 0x0a, 0x03, 0x47, 0x65, 0x74, 0x12, 0x21, 0x2e,
+	0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
+	0x74, 0x50, 0x61, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+	0x1a, 0x17, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31,
+	0x2e, 0x50, 0x61, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x37, 0x0a, 0x03, 0x50, 0x75, 0x74,
+	0x12, 0x17, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31,
+	0x2e, 0x50, 0x61, 0x74, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x1a, 0x17, 0x2e, 0x74, 0x76, 0x69, 0x78,
+	0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x49, 0x6e,
+	0x66, 0x6f, 0x12, 0x48, 0x0a, 0x0c, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74, 0x65, 0x4e,
+	0x41, 0x52, 0x12, 0x13, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e,
+	0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x1a, 0x23, 0x2e, 0x74, 0x76, 0x69, 0x78, 0x2e, 0x73,
+	0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x6c, 0x63, 0x75, 0x6c, 0x61, 0x74,
+	0x65, 0x4e, 0x41, 0x52, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x28, 0x5a, 0x26,
+	0x63, 0x6f, 0x64, 0x65, 0x2e, 0x74, 0x76, 0x6c, 0x2e, 0x66, 0x79, 0x69, 0x2f, 0x74, 0x76, 0x69,
+	0x78, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x3b, 0x73,
+	0x74, 0x6f, 0x72, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_tvix_store_protos_rpc_pathinfo_proto_rawDescOnce sync.Once
+	file_tvix_store_protos_rpc_pathinfo_proto_rawDescData = file_tvix_store_protos_rpc_pathinfo_proto_rawDesc
+)
+
+func file_tvix_store_protos_rpc_pathinfo_proto_rawDescGZIP() []byte {
+	file_tvix_store_protos_rpc_pathinfo_proto_rawDescOnce.Do(func() {
+		file_tvix_store_protos_rpc_pathinfo_proto_rawDescData = protoimpl.X.CompressGZIP(file_tvix_store_protos_rpc_pathinfo_proto_rawDescData)
+	})
+	return file_tvix_store_protos_rpc_pathinfo_proto_rawDescData
+}
+
+var file_tvix_store_protos_rpc_pathinfo_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_tvix_store_protos_rpc_pathinfo_proto_goTypes = []interface{}{
+	(*GetPathInfoRequest)(nil),   // 0: tvix.store.v1.GetPathInfoRequest
+	(*CalculateNARResponse)(nil), // 1: tvix.store.v1.CalculateNARResponse
+	(*PathInfo)(nil),             // 2: tvix.store.v1.PathInfo
+	(*Node)(nil),                 // 3: tvix.store.v1.Node
+}
+var file_tvix_store_protos_rpc_pathinfo_proto_depIdxs = []int32{
+	0, // 0: tvix.store.v1.PathInfoService.Get:input_type -> tvix.store.v1.GetPathInfoRequest
+	2, // 1: tvix.store.v1.PathInfoService.Put:input_type -> tvix.store.v1.PathInfo
+	3, // 2: tvix.store.v1.PathInfoService.CalculateNAR:input_type -> tvix.store.v1.Node
+	2, // 3: tvix.store.v1.PathInfoService.Get:output_type -> tvix.store.v1.PathInfo
+	2, // 4: tvix.store.v1.PathInfoService.Put:output_type -> tvix.store.v1.PathInfo
+	1, // 5: tvix.store.v1.PathInfoService.CalculateNAR:output_type -> tvix.store.v1.CalculateNARResponse
+	3, // [3:6] is the sub-list for method output_type
+	0, // [0:3] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_tvix_store_protos_rpc_pathinfo_proto_init() }
+func file_tvix_store_protos_rpc_pathinfo_proto_init() {
+	if File_tvix_store_protos_rpc_pathinfo_proto != nil {
+		return
+	}
+	file_tvix_store_protos_pathinfo_proto_init()
+	if !protoimpl.UnsafeEnabled {
+		file_tvix_store_protos_rpc_pathinfo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*GetPathInfoRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_tvix_store_protos_rpc_pathinfo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CalculateNARResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	file_tvix_store_protos_rpc_pathinfo_proto_msgTypes[0].OneofWrappers = []interface{}{
+		(*GetPathInfoRequest_ByOutputHash)(nil),
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_tvix_store_protos_rpc_pathinfo_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_tvix_store_protos_rpc_pathinfo_proto_goTypes,
+		DependencyIndexes: file_tvix_store_protos_rpc_pathinfo_proto_depIdxs,
+		MessageInfos:      file_tvix_store_protos_rpc_pathinfo_proto_msgTypes,
+	}.Build()
+	File_tvix_store_protos_rpc_pathinfo_proto = out.File
+	file_tvix_store_protos_rpc_pathinfo_proto_rawDesc = nil
+	file_tvix_store_protos_rpc_pathinfo_proto_goTypes = nil
+	file_tvix_store_protos_rpc_pathinfo_proto_depIdxs = nil
+}
diff --git a/tvix/store/protos/rpc_pathinfo.proto b/tvix/store/protos/rpc_pathinfo.proto
new file mode 100644
index 000000000000..991abf6efa99
--- /dev/null
+++ b/tvix/store/protos/rpc_pathinfo.proto
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+syntax = "proto3";
+
+package tvix.store.v1;
+
+import "tvix/store/protos/pathinfo.proto";
+
+option go_package = "code.tvl.fyi/tvix/store/protos;storev1";
+
+service PathInfoService {
+    // Return a PathInfo message, identified by the decoded nixbase32 part
+    // of a Nix output path.
+    //
+    // To substitute /nix/store/xm35nga2g20mz5sm5l6n8v3bdm86yj83-
+    // cowsay-3.04 the bytes in the request would be
+    // nixbase32dec("xm35nga2g20mz5sm5l6n8v3bdm86yj83").
+    rpc Get(GetPathInfoRequest) returns (PathInfo);
+
+    // Put uploads a PathInfo object to the remote end. It MUST not return
+    // until the PathInfo object has been written on the the remote end.
+    //
+    // The remote end MAY check if a potential DirectoryNode has already been
+    // uploaded.
+    //
+    // Uploading clients SHOULD obviously not steer other machines to try to
+    // substitute before from the remote end before having finished uploading
+    // PathInfo, Directories and Blobs.
+    // The returned PathInfo object MAY contain additional narinfo signatures,
+    // but is otherwise left untouched.
+    rpc Put(PathInfo) returns (PathInfo);
+
+
+    // Calculate the NAR representation of the contents specified by the
+    // root_node. The calculation SHOULD be cached server-side for subsequent
+    // requests.
+    //
+    // All references (to blobs or Directory messages) MUST already exist in
+    // the store.
+    //
+    // The method can be used to produce a Nix fixed-output path, which
+    // contains the (compressed) sha256 of the NAR content representation in
+    // the root_node name (suffixed with the name).
+    //
+    // It can also be used to calculate arbitrary NAR hashes of output paths,
+    // in case a legacy Nix Binary Cache frontend is provided.
+    rpc CalculateNAR(Node) returns (CalculateNARResponse);
+}
+
+// GetPathInfoRequest describes the lookup parameters that can be used to
+// lookup a PathInfo objects.
+// Currently, only a lookup by output hash is supported.
+message GetPathInfoRequest {
+    oneof by_what {
+      // The output hash of a nix path (20 bytes).
+      // This is the nixbase32-decoded portion of a Nix output path, so to substitute
+      // /nix/store/xm35nga2g20mz5sm5l6n8v3bdm86yj83-cowsay-3.04
+      // this field would contain nixbase32dec("xm35nga2g20mz5sm5l6n8v3bdm86yj83").
+      bytes by_output_hash = 1;
+    };
+}
+
+// CalculateNARResponse is the response returned by the CalculateNAR request.
+//
+// It contains the size of the NAR representation (in bytes), and the sha56
+// digest.
+message CalculateNARResponse {
+    // This size of the NAR file, in bytes.
+    uint64 nar_size = 1;
+
+    // The sha256 of the NAR file representation.
+    bytes nar_sha256 = 2;
+}
diff --git a/tvix/store/protos/rpc_pathinfo_grpc.pb.go b/tvix/store/protos/rpc_pathinfo_grpc.pb.go
new file mode 100644
index 000000000000..5a858a3324c6
--- /dev/null
+++ b/tvix/store/protos/rpc_pathinfo_grpc.pb.go
@@ -0,0 +1,246 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2022 The Tvix Authors
+
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc             (unknown)
+// source: tvix/store/protos/rpc_pathinfo.proto
+
+package storev1
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+	PathInfoService_Get_FullMethodName          = "/tvix.store.v1.PathInfoService/Get"
+	PathInfoService_Put_FullMethodName          = "/tvix.store.v1.PathInfoService/Put"
+	PathInfoService_CalculateNAR_FullMethodName = "/tvix.store.v1.PathInfoService/CalculateNAR"
+)
+
+// PathInfoServiceClient is the client API for PathInfoService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type PathInfoServiceClient interface {
+	// Return a PathInfo message, identified by the decoded nixbase32 part
+	// of a Nix output path.
+	//
+	// To substitute /nix/store/xm35nga2g20mz5sm5l6n8v3bdm86yj83-
+	// cowsay-3.04 the bytes in the request would be
+	// nixbase32dec("xm35nga2g20mz5sm5l6n8v3bdm86yj83").
+	Get(ctx context.Context, in *GetPathInfoRequest, opts ...grpc.CallOption) (*PathInfo, error)
+	// Put uploads a PathInfo object to the remote end. It MUST not return
+	// until the PathInfo object has been written on the the remote end.
+	//
+	// The remote end MAY check if a potential DirectoryNode has already been
+	// uploaded.
+	//
+	// Uploading clients SHOULD obviously not steer other machines to try to
+	// substitute before from the remote end before having finished uploading
+	// PathInfo, Directories and Blobs.
+	// The returned PathInfo object MAY contain additional narinfo signatures,
+	// but is otherwise left untouched.
+	Put(ctx context.Context, in *PathInfo, opts ...grpc.CallOption) (*PathInfo, error)
+	// Calculate the NAR representation of the contents specified by the
+	// root_node. The calculation SHOULD be cached server-side for subsequent
+	// requests.
+	//
+	// All references (to blobs or Directory messages) MUST already exist in
+	// the store.
+	//
+	// The method can be used to produce a Nix fixed-output path, which
+	// contains the (compressed) sha256 of the NAR content representation in
+	// the root_node name (suffixed with the name).
+	//
+	// It can also be used to calculate arbitrary NAR hashes of output paths,
+	// in case a legacy Nix Binary Cache frontend is provided.
+	CalculateNAR(ctx context.Context, in *Node, opts ...grpc.CallOption) (*CalculateNARResponse, error)
+}
+
+type pathInfoServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewPathInfoServiceClient(cc grpc.ClientConnInterface) PathInfoServiceClient {
+	return &pathInfoServiceClient{cc}
+}
+
+func (c *pathInfoServiceClient) Get(ctx context.Context, in *GetPathInfoRequest, opts ...grpc.CallOption) (*PathInfo, error) {
+	out := new(PathInfo)
+	err := c.cc.Invoke(ctx, PathInfoService_Get_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *pathInfoServiceClient) Put(ctx context.Context, in *PathInfo, opts ...grpc.CallOption) (*PathInfo, error) {
+	out := new(PathInfo)
+	err := c.cc.Invoke(ctx, PathInfoService_Put_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *pathInfoServiceClient) CalculateNAR(ctx context.Context, in *Node, opts ...grpc.CallOption) (*CalculateNARResponse, error) {
+	out := new(CalculateNARResponse)
+	err := c.cc.Invoke(ctx, PathInfoService_CalculateNAR_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// PathInfoServiceServer is the server API for PathInfoService service.
+// All implementations must embed UnimplementedPathInfoServiceServer
+// for forward compatibility
+type PathInfoServiceServer interface {
+	// Return a PathInfo message, identified by the decoded nixbase32 part
+	// of a Nix output path.
+	//
+	// To substitute /nix/store/xm35nga2g20mz5sm5l6n8v3bdm86yj83-
+	// cowsay-3.04 the bytes in the request would be
+	// nixbase32dec("xm35nga2g20mz5sm5l6n8v3bdm86yj83").
+	Get(context.Context, *GetPathInfoRequest) (*PathInfo, error)
+	// Put uploads a PathInfo object to the remote end. It MUST not return
+	// until the PathInfo object has been written on the the remote end.
+	//
+	// The remote end MAY check if a potential DirectoryNode has already been
+	// uploaded.
+	//
+	// Uploading clients SHOULD obviously not steer other machines to try to
+	// substitute before from the remote end before having finished uploading
+	// PathInfo, Directories and Blobs.
+	// The returned PathInfo object MAY contain additional narinfo signatures,
+	// but is otherwise left untouched.
+	Put(context.Context, *PathInfo) (*PathInfo, error)
+	// Calculate the NAR representation of the contents specified by the
+	// root_node. The calculation SHOULD be cached server-side for subsequent
+	// requests.
+	//
+	// All references (to blobs or Directory messages) MUST already exist in
+	// the store.
+	//
+	// The method can be used to produce a Nix fixed-output path, which
+	// contains the (compressed) sha256 of the NAR content representation in
+	// the root_node name (suffixed with the name).
+	//
+	// It can also be used to calculate arbitrary NAR hashes of output paths,
+	// in case a legacy Nix Binary Cache frontend is provided.
+	CalculateNAR(context.Context, *Node) (*CalculateNARResponse, error)
+	mustEmbedUnimplementedPathInfoServiceServer()
+}
+
+// UnimplementedPathInfoServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedPathInfoServiceServer struct {
+}
+
+func (UnimplementedPathInfoServiceServer) Get(context.Context, *GetPathInfoRequest) (*PathInfo, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method Get not implemented")
+}
+func (UnimplementedPathInfoServiceServer) Put(context.Context, *PathInfo) (*PathInfo, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method Put not implemented")
+}
+func (UnimplementedPathInfoServiceServer) CalculateNAR(context.Context, *Node) (*CalculateNARResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method CalculateNAR not implemented")
+}
+func (UnimplementedPathInfoServiceServer) mustEmbedUnimplementedPathInfoServiceServer() {}
+
+// UnsafePathInfoServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to PathInfoServiceServer will
+// result in compilation errors.
+type UnsafePathInfoServiceServer interface {
+	mustEmbedUnimplementedPathInfoServiceServer()
+}
+
+func RegisterPathInfoServiceServer(s grpc.ServiceRegistrar, srv PathInfoServiceServer) {
+	s.RegisterService(&PathInfoService_ServiceDesc, srv)
+}
+
+func _PathInfoService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(GetPathInfoRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PathInfoServiceServer).Get(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PathInfoService_Get_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PathInfoServiceServer).Get(ctx, req.(*GetPathInfoRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _PathInfoService_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(PathInfo)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PathInfoServiceServer).Put(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PathInfoService_Put_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PathInfoServiceServer).Put(ctx, req.(*PathInfo))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _PathInfoService_CalculateNAR_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(Node)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(PathInfoServiceServer).CalculateNAR(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: PathInfoService_CalculateNAR_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(PathInfoServiceServer).CalculateNAR(ctx, req.(*Node))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// PathInfoService_ServiceDesc is the grpc.ServiceDesc for PathInfoService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var PathInfoService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "tvix.store.v1.PathInfoService",
+	HandlerType: (*PathInfoServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "Get",
+			Handler:    _PathInfoService_Get_Handler,
+		},
+		{
+			MethodName: "Put",
+			Handler:    _PathInfoService_Put_Handler,
+		},
+		{
+			MethodName: "CalculateNAR",
+			Handler:    _PathInfoService_CalculateNAR_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "tvix/store/protos/rpc_pathinfo.proto",
+}
diff --git a/tvix/store/src/bin/tvix-store.rs b/tvix/store/src/bin/tvix-store.rs
new file mode 100644
index 000000000000..8c278c433997
--- /dev/null
+++ b/tvix/store/src/bin/tvix-store.rs
@@ -0,0 +1,206 @@
+use clap::Subcommand;
+use data_encoding::BASE64;
+use futures::future::try_join_all;
+use std::io;
+use std::path::Path;
+use std::path::PathBuf;
+use std::sync::Arc;
+use tracing_subscriber::prelude::*;
+use tvix_store::blobservice::GRPCBlobService;
+use tvix_store::blobservice::SledBlobService;
+use tvix_store::directoryservice::GRPCDirectoryService;
+use tvix_store::directoryservice::SledDirectoryService;
+use tvix_store::nar::GRPCNARCalculationService;
+use tvix_store::nar::NonCachingNARCalculationService;
+use tvix_store::pathinfoservice::GRPCPathInfoService;
+use tvix_store::pathinfoservice::SledPathInfoService;
+use tvix_store::proto::blob_service_client::BlobServiceClient;
+use tvix_store::proto::blob_service_server::BlobServiceServer;
+use tvix_store::proto::directory_service_client::DirectoryServiceClient;
+use tvix_store::proto::directory_service_server::DirectoryServiceServer;
+use tvix_store::proto::node::Node;
+use tvix_store::proto::path_info_service_client::PathInfoServiceClient;
+use tvix_store::proto::path_info_service_server::PathInfoServiceServer;
+use tvix_store::proto::GRPCBlobServiceWrapper;
+use tvix_store::proto::GRPCDirectoryServiceWrapper;
+use tvix_store::proto::GRPCPathInfoServiceWrapper;
+use tvix_store::TvixStoreIO;
+
+#[cfg(feature = "reflection")]
+use tvix_store::proto::FILE_DESCRIPTOR_SET;
+
+use clap::Parser;
+use tonic::{transport::Server, Result};
+use tracing::{info, Level};
+
+#[derive(Parser)]
+#[command(author, version, about, long_about = None)]
+struct Cli {
+    /// Whether to log in JSON
+    #[arg(long)]
+    json: bool,
+
+    #[arg(long)]
+    log_level: Option<Level>,
+
+    #[command(subcommand)]
+    command: Commands,
+}
+
+#[derive(Subcommand)]
+enum Commands {
+    /// Runs the tvix-store daemon.
+    Daemon {
+        #[arg(long, short = 'l')]
+        listen_address: Option<String>,
+    },
+    /// Imports a list of paths into the store (not using the daemon)
+    Import {
+        #[clap(value_name = "PATH")]
+        paths: Vec<PathBuf>,
+    },
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let cli = Cli::parse();
+
+    // configure log settings
+    let level = cli.log_level.unwrap_or(Level::INFO);
+
+    let subscriber = tracing_subscriber::registry()
+        .with(if cli.json {
+            Some(
+                tracing_subscriber::fmt::Layer::new()
+                    .with_writer(io::stdout.with_max_level(level))
+                    .json(),
+            )
+        } else {
+            None
+        })
+        .with(if !cli.json {
+            Some(
+                tracing_subscriber::fmt::Layer::new()
+                    .with_writer(io::stdout.with_max_level(level))
+                    .pretty(),
+            )
+        } else {
+            None
+        });
+
+    tracing::subscriber::set_global_default(subscriber).expect("Unable to set global subscriber");
+
+    match cli.command {
+        Commands::Daemon { listen_address } => {
+            // initialize stores
+            let blob_service = SledBlobService::new("blobs.sled".into())?;
+            let directory_service = SledDirectoryService::new("directories.sled".into())?;
+            let path_info_service = SledPathInfoService::new("pathinfo.sled".into())?;
+
+            let listen_address = listen_address
+                .unwrap_or_else(|| "[::]:8000".to_string())
+                .parse()
+                .unwrap();
+
+            let mut server = Server::builder();
+
+            let nar_calculation_service = NonCachingNARCalculationService::new(
+                blob_service.clone(),
+                directory_service.clone(),
+            );
+
+            #[allow(unused_mut)]
+            let mut router = server
+                .add_service(BlobServiceServer::new(GRPCBlobServiceWrapper::from(
+                    blob_service,
+                )))
+                .add_service(DirectoryServiceServer::new(
+                    GRPCDirectoryServiceWrapper::from(directory_service),
+                ))
+                .add_service(PathInfoServiceServer::new(GRPCPathInfoServiceWrapper::new(
+                    path_info_service,
+                    nar_calculation_service,
+                )));
+
+            #[cfg(feature = "reflection")]
+            {
+                let reflection_svc = tonic_reflection::server::Builder::configure()
+                    .register_encoded_file_descriptor_set(FILE_DESCRIPTOR_SET)
+                    .build()?;
+                router = router.add_service(reflection_svc);
+            }
+
+            info!("tvix-store listening on {}", listen_address);
+
+            router.serve(listen_address).await?;
+        }
+        Commands::Import { paths } => {
+            let blob_service = GRPCBlobService::from_client(
+                BlobServiceClient::connect("http://[::1]:8000").await?,
+            );
+            let directory_service = GRPCDirectoryService::from_client(
+                DirectoryServiceClient::connect("http://[::1]:8000").await?,
+            );
+            let path_info_service_client =
+                PathInfoServiceClient::connect("http://[::1]:8000").await?;
+            let path_info_service =
+                GRPCPathInfoService::from_client(path_info_service_client.clone());
+            let nar_calculation_service =
+                GRPCNARCalculationService::from_client(path_info_service_client);
+
+            let io = Arc::new(TvixStoreIO::new(
+                blob_service,
+                directory_service,
+                path_info_service,
+                nar_calculation_service,
+            ));
+
+            let tasks = paths
+                .iter()
+                .map(|path| {
+                    let io_move = io.clone();
+                    let path = path.clone();
+                    let task: tokio::task::JoinHandle<Result<(), io::Error>> =
+                        tokio::task::spawn_blocking(move || {
+                            let path_info = io_move.import_path_with_pathinfo(&path)?;
+                            print_node(&path_info.node.unwrap().node.unwrap(), &path);
+                            Ok(())
+                        });
+                    task
+                })
+                .collect::<Vec<tokio::task::JoinHandle<Result<(), io::Error>>>>();
+
+            try_join_all(tasks).await?;
+        }
+    };
+    Ok(())
+}
+
+fn print_node(node: &Node, path: &Path) {
+    match node {
+        Node::Directory(directory_node) => {
+            info!(
+                path = ?path,
+                name = directory_node.name,
+                digest = BASE64.encode(&directory_node.digest),
+                "import successful",
+            )
+        }
+        Node::File(file_node) => {
+            info!(
+                path = ?path,
+                name = file_node.name,
+                digest = BASE64.encode(&file_node.digest),
+                "import successful"
+            )
+        }
+        Node::Symlink(symlink_node) => {
+            info!(
+                path = ?path,
+                name = symlink_node.name,
+                target = symlink_node.target,
+                "import successful"
+            )
+        }
+    }
+}
diff --git a/tvix/store/src/blobservice/grpc.rs b/tvix/store/src/blobservice/grpc.rs
new file mode 100644
index 000000000000..0b08fbf46ad9
--- /dev/null
+++ b/tvix/store/src/blobservice/grpc.rs
@@ -0,0 +1,217 @@
+use super::{BlobService, BlobWriter};
+use crate::{proto, B3Digest};
+use futures::sink::{SinkExt, SinkMapErr};
+use std::{collections::VecDeque, io};
+use tokio::task::JoinHandle;
+use tokio_stream::{wrappers::ReceiverStream, StreamExt};
+use tokio_util::{
+    io::{CopyToBytes, SinkWriter, SyncIoBridge},
+    sync::{PollSendError, PollSender},
+};
+use tonic::{transport::Channel, Code, Status, Streaming};
+use tracing::instrument;
+
+/// Connects to a (remote) tvix-store BlobService over gRPC.
+#[derive(Clone)]
+pub struct GRPCBlobService {
+    /// A handle into the active tokio runtime. Necessary to spawn tasks.
+    tokio_handle: tokio::runtime::Handle,
+
+    /// The internal reference to a gRPC client.
+    /// Cloning it is cheap, and it internally handles concurrent requests.
+    grpc_client: proto::blob_service_client::BlobServiceClient<Channel>,
+}
+
+impl GRPCBlobService {
+    /// construct a [GRPCBlobService] from a [proto::blob_service_client::BlobServiceClient<Channel>],
+    /// and a [tokio::runtime::Handle].
+    pub fn new(
+        grpc_client: proto::blob_service_client::BlobServiceClient<Channel>,
+        tokio_handle: tokio::runtime::Handle,
+    ) -> Self {
+        Self {
+            tokio_handle,
+            grpc_client,
+        }
+    }
+    /// construct a [GRPCBlobService] from a [proto::blob_service_client::BlobServiceClient<Channel>].
+    /// panics if called outside the context of a tokio runtime.
+    pub fn from_client(
+        grpc_client: proto::blob_service_client::BlobServiceClient<Channel>,
+    ) -> Self {
+        Self {
+            tokio_handle: tokio::runtime::Handle::current(),
+            grpc_client,
+        }
+    }
+}
+
+impl BlobService for GRPCBlobService {
+    type BlobReader = Box<dyn io::Read + Send>;
+    type BlobWriter = GRPCBlobWriter;
+
+    #[instrument(skip(self, digest), fields(blob.digest=%digest))]
+    fn has(&self, digest: &B3Digest) -> Result<bool, crate::Error> {
+        // Get a new handle to the gRPC client, and copy the digest.
+        let mut grpc_client = self.grpc_client.clone();
+        let digest = digest.clone();
+
+        let task: tokio::task::JoinHandle<Result<_, Status>> =
+            self.tokio_handle.spawn(async move {
+                Ok(grpc_client
+                    .stat(proto::StatBlobRequest {
+                        digest: digest.to_vec(),
+                        ..Default::default()
+                    })
+                    .await?
+                    .into_inner())
+            });
+
+        match self.tokio_handle.block_on(task)? {
+            Ok(_blob_meta) => Ok(true),
+            Err(e) if e.code() == Code::NotFound => Ok(false),
+            Err(e) => Err(crate::Error::StorageError(e.to_string())),
+        }
+    }
+
+    // On success, this returns a Ok(Some(io::Read)), which can be used to read
+    // the contents of the Blob, identified by the digest.
+    fn open_read(&self, digest: &B3Digest) -> Result<Option<Self::BlobReader>, crate::Error> {
+        // Get a new handle to the gRPC client, and copy the digest.
+        let mut grpc_client = self.grpc_client.clone();
+        let digest = digest.clone();
+
+        // Construct the task that'll send out the request and return the stream
+        // the gRPC client should use to send [proto::BlobChunk], or an error if
+        // the blob doesn't exist.
+        let task: tokio::task::JoinHandle<Result<Streaming<proto::BlobChunk>, Status>> =
+            self.tokio_handle.spawn(async move {
+                let stream = grpc_client
+                    .read(proto::ReadBlobRequest {
+                        digest: digest.to_vec(),
+                    })
+                    .await?
+                    .into_inner();
+
+                Ok(stream)
+            });
+
+        // This runs the task to completion, which on success will return a stream.
+        // On reading from it, we receive individual [proto::BlobChunk], so we
+        // massage this to a stream of bytes,
+        // then create an [AsyncRead], which we'll turn into a [io::Read],
+        // that's returned from the function.
+        match self.tokio_handle.block_on(task)? {
+            Ok(stream) => {
+                // map the stream of proto::BlobChunk to bytes.
+                let data_stream = stream.map(|x| {
+                    x.map(|x| VecDeque::from(x.data))
+                        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
+                });
+
+                // Use StreamReader::new to convert to an AsyncRead.
+                let data_reader = tokio_util::io::StreamReader::new(data_stream);
+
+                // Use SyncIoBridge to turn it into a sync Read.
+                let sync_reader = tokio_util::io::SyncIoBridge::new(data_reader);
+                Ok(Some(Box::new(sync_reader)))
+            }
+            Err(e) if e.code() == Code::NotFound => Ok(None),
+            Err(e) => Err(crate::Error::StorageError(e.to_string())),
+        }
+    }
+
+    /// Returns a [Self::BlobWriter], that'll internally wrap each write in a
+    // [proto::BlobChunk] and which is passed to the
+    fn open_write(&self) -> Result<Self::BlobWriter, crate::Error> {
+        let mut grpc_client = self.grpc_client.clone();
+
+        // set up an mpsc channel passing around Bytes.
+        let (tx, rx) = tokio::sync::mpsc::channel::<bytes::Bytes>(10);
+
+        // bytes arriving on the RX side are wrapped inside a
+        // [proto::BlobChunk], and a [ReceiverStream] is constructed.
+        let blobchunk_stream =
+            ReceiverStream::new(rx).map(|x| proto::BlobChunk { data: x.to_vec() });
+
+        // That receiver stream is used as a stream in the gRPC BlobService.put rpc call.
+        let task: tokio::task::JoinHandle<Result<_, Status>> = self
+            .tokio_handle
+            .spawn(async move { Ok(grpc_client.put(blobchunk_stream).await?.into_inner()) });
+
+        // The tx part of the channel is converted to a sink of byte chunks.
+
+        // We need to make this a function pointer, not a closure.
+        fn convert_error(_: PollSendError<bytes::Bytes>) -> io::Error {
+            io::Error::from(io::ErrorKind::BrokenPipe)
+        }
+
+        let sink = PollSender::new(tx)
+            .sink_map_err(convert_error as fn(PollSendError<bytes::Bytes>) -> io::Error);
+        // We need to explicitly cast here, otherwise rustc does error with "expected fn pointer, found fn item"
+
+        // … which is turned into an [tokio::io::AsyncWrite].
+        let async_writer = SinkWriter::new(CopyToBytes::new(sink));
+        // … which is then turned into a [io::Write].
+        let writer = SyncIoBridge::new(async_writer);
+
+        Ok(GRPCBlobWriter {
+            tokio_handle: self.tokio_handle.clone(), // TODO: is the clone() ok here?
+            task,
+            inner_writer: writer,
+        })
+    }
+}
+
+type BridgedWriter = SyncIoBridge<
+    SinkWriter<
+        CopyToBytes<
+            SinkMapErr<PollSender<bytes::Bytes>, fn(PollSendError<bytes::Bytes>) -> io::Error>,
+        >,
+    >,
+>;
+
+pub struct GRPCBlobWriter {
+    /// A handle into the active tokio runtime. Necessary to block on the task
+    /// containing the put request.
+    tokio_handle: tokio::runtime::Handle,
+
+    /// The task containing the put request.
+    task: JoinHandle<Result<proto::PutBlobResponse, Status>>,
+
+    /// The inner Writer.
+    inner_writer: BridgedWriter,
+}
+
+impl BlobWriter for GRPCBlobWriter {
+    fn close(mut self) -> Result<B3Digest, crate::Error> {
+        // invoke shutdown, so the inner writer closes its internal tx side of
+        // the channel.
+        self.inner_writer
+            .shutdown()
+            .map_err(|e| crate::Error::StorageError(e.to_string()))?;
+
+        // block on the RPC call to return.
+        // This ensures all chunks are sent out, and have been received by the
+        // backend.
+        match self.tokio_handle.block_on(self.task)? {
+            Ok(resp) => {
+                // return the digest from the response.
+                B3Digest::from_vec(resp.digest).map_err(|_| {
+                    crate::Error::StorageError("invalid root digest length in response".to_string())
+                })
+            }
+            Err(e) => Err(crate::Error::StorageError(e.to_string())),
+        }
+    }
+}
+
+impl io::Write for GRPCBlobWriter {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.inner_writer.write(buf)
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.inner_writer.flush()
+    }
+}
diff --git a/tvix/store/src/blobservice/memory.rs b/tvix/store/src/blobservice/memory.rs
new file mode 100644
index 000000000000..1ee59d108743
--- /dev/null
+++ b/tvix/store/src/blobservice/memory.rs
@@ -0,0 +1,76 @@
+use std::io::Cursor;
+use std::{
+    collections::HashMap,
+    sync::{Arc, RwLock},
+};
+use tracing::{instrument, warn};
+
+use super::{BlobService, BlobWriter};
+use crate::{B3Digest, Error};
+
+#[derive(Clone, Default)]
+pub struct MemoryBlobService {
+    db: Arc<RwLock<HashMap<B3Digest, Vec<u8>>>>,
+}
+
+impl BlobService for MemoryBlobService {
+    type BlobReader = Cursor<Vec<u8>>;
+    type BlobWriter = MemoryBlobWriter;
+
+    #[instrument(skip(self, digest), fields(blob.digest=%digest))]
+    fn has(&self, digest: &B3Digest) -> Result<bool, Error> {
+        let db = self.db.read().unwrap();
+        Ok(db.contains_key(digest))
+    }
+
+    fn open_read(&self, digest: &B3Digest) -> Result<Option<Self::BlobReader>, Error> {
+        let db = self.db.read().unwrap();
+
+        Ok(db.get(digest).map(|x| Cursor::new(x.clone())))
+    }
+
+    #[instrument(skip(self))]
+    fn open_write(&self) -> Result<Self::BlobWriter, Error> {
+        Ok(MemoryBlobWriter::new(self.db.clone()))
+    }
+}
+
+pub struct MemoryBlobWriter {
+    db: Arc<RwLock<HashMap<B3Digest, Vec<u8>>>>,
+
+    buf: Vec<u8>,
+}
+
+impl MemoryBlobWriter {
+    fn new(db: Arc<RwLock<HashMap<B3Digest, Vec<u8>>>>) -> Self {
+        Self {
+            buf: Vec::new(),
+            db,
+        }
+    }
+}
+impl std::io::Write for MemoryBlobWriter {
+    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
+        self.buf.write(buf)
+    }
+
+    fn flush(&mut self) -> std::io::Result<()> {
+        self.buf.flush()
+    }
+}
+
+impl BlobWriter for MemoryBlobWriter {
+    fn close(self) -> Result<B3Digest, Error> {
+        // in this memory implementation, we don't actually bother hashing
+        // incrementally while writing, but do it at the end.
+        let mut hasher = blake3::Hasher::new();
+        hasher.update(&self.buf);
+        let digest = B3Digest::from_vec(hasher.finalize().as_bytes().to_vec()).unwrap();
+
+        // open the database for writing.
+        let mut db = self.db.write()?;
+        db.insert(digest.clone(), self.buf);
+
+        Ok(digest)
+    }
+}
diff --git a/tvix/store/src/blobservice/mod.rs b/tvix/store/src/blobservice/mod.rs
new file mode 100644
index 000000000000..c5a2de124656
--- /dev/null
+++ b/tvix/store/src/blobservice/mod.rs
@@ -0,0 +1,41 @@
+use std::io;
+
+use crate::{B3Digest, Error};
+
+mod grpc;
+mod memory;
+mod sled;
+
+pub use self::grpc::GRPCBlobService;
+pub use self::memory::MemoryBlobService;
+pub use self::sled::SledBlobService;
+
+/// The base trait all BlobService services need to implement.
+/// It provides functions to check whether a given blob exists,
+/// a way to get a [io::Read] to a blob, and a method to initiate writing a new
+/// Blob, which returns a [BlobWriter], that can be used
+pub trait BlobService {
+    type BlobReader: io::Read + Send + std::marker::Unpin;
+    type BlobWriter: BlobWriter + Send;
+
+    /// Check if the service has the blob, by its content hash.
+    fn has(&self, digest: &B3Digest) -> Result<bool, Error>;
+
+    /// Request a blob from the store, by its content hash. Returns a Option<BlobReader>.
+    fn open_read(&self, digest: &B3Digest) -> Result<Option<Self::BlobReader>, Error>;
+
+    /// Insert a new blob into the store. Returns a [BlobWriter], which
+    /// implements [io::Write] and a [BlobWriter::close].
+    /// TODO: is there any reason we want this to be a Result<>, and not just T?
+    fn open_write(&self) -> Result<Self::BlobWriter, Error>;
+}
+
+/// A [io::Write] that you need to close() afterwards, and get back the digest
+/// of the written blob.
+pub trait BlobWriter: io::Write {
+    /// Signal there's no more data to be written, and return the digest of the
+    /// contents written.
+    ///
+    /// This consumes self, so it's not possible to close twice.
+    fn close(self) -> Result<B3Digest, Error>;
+}
diff --git a/tvix/store/src/blobservice/sled.rs b/tvix/store/src/blobservice/sled.rs
new file mode 100644
index 000000000000..2b090335344d
--- /dev/null
+++ b/tvix/store/src/blobservice/sled.rs
@@ -0,0 +1,94 @@
+use super::{BlobService, BlobWriter};
+use crate::{B3Digest, Error};
+use std::{
+    io::{self, Cursor},
+    path::PathBuf,
+};
+use tracing::instrument;
+
+#[derive(Clone)]
+pub struct SledBlobService {
+    db: sled::Db,
+}
+
+impl SledBlobService {
+    pub fn new(p: PathBuf) -> Result<Self, sled::Error> {
+        let config = sled::Config::default().use_compression(true).path(p);
+        let db = config.open()?;
+
+        Ok(Self { db })
+    }
+
+    pub fn new_temporary() -> Result<Self, sled::Error> {
+        let config = sled::Config::default().temporary(true);
+        let db = config.open()?;
+
+        Ok(Self { db })
+    }
+}
+
+impl BlobService for SledBlobService {
+    type BlobReader = Cursor<Vec<u8>>;
+    type BlobWriter = SledBlobWriter;
+
+    #[instrument(skip(self), fields(blob.digest=%digest))]
+    fn has(&self, digest: &B3Digest) -> Result<bool, Error> {
+        match self.db.contains_key(digest.to_vec()) {
+            Ok(has) => Ok(has),
+            Err(e) => Err(Error::StorageError(e.to_string())),
+        }
+    }
+
+    #[instrument(skip(self), fields(blob.digest=%digest))]
+    fn open_read(&self, digest: &B3Digest) -> Result<Option<Self::BlobReader>, Error> {
+        match self.db.get(digest.to_vec()) {
+            Ok(None) => Ok(None),
+            Ok(Some(data)) => Ok(Some(Cursor::new(data[..].to_vec()))),
+            Err(e) => Err(Error::StorageError(e.to_string())),
+        }
+    }
+
+    #[instrument(skip(self))]
+    fn open_write(&self) -> Result<Self::BlobWriter, Error> {
+        Ok(SledBlobWriter::new(self.db.clone()))
+    }
+}
+
+pub struct SledBlobWriter {
+    db: sled::Db,
+    buf: Vec<u8>,
+    hasher: blake3::Hasher,
+}
+
+impl SledBlobWriter {
+    pub fn new(db: sled::Db) -> Self {
+        Self {
+            buf: Vec::default(),
+            db,
+            hasher: blake3::Hasher::new(),
+        }
+    }
+}
+
+impl io::Write for SledBlobWriter {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        let bytes_written = self.buf.write(buf)?;
+        self.hasher.write(&buf[..bytes_written])
+    }
+
+    fn flush(&mut self) -> io::Result<()> {
+        self.buf.flush()
+    }
+}
+
+impl BlobWriter for SledBlobWriter {
+    fn close(self) -> Result<B3Digest, Error> {
+        let digest = self.hasher.finalize();
+        self.db
+            .insert(digest.as_bytes(), self.buf)
+            .map_err(|e| Error::StorageError(format!("unable to insert blob: {}", e)))?;
+
+        // We know self.hasher is doing blake3 hashing, so this won't fail.
+        Ok(B3Digest::from_vec(digest.as_bytes().to_vec()).unwrap())
+    }
+}
diff --git a/tvix/store/src/digests.rs b/tvix/store/src/digests.rs
new file mode 100644
index 000000000000..441a059ee0b6
--- /dev/null
+++ b/tvix/store/src/digests.rs
@@ -0,0 +1,49 @@
+use data_encoding::BASE64;
+use thiserror::Error;
+
+// FUTUREWORK: make generic
+
+#[derive(PartialEq, Eq, Hash, Debug)]
+pub struct B3Digest(Vec<u8>);
+
+// TODO: allow converting these errors to crate::Error
+#[derive(Error, Debug)]
+pub enum Error {
+    #[error("invalid digest length: {0}")]
+    InvalidDigestLen(usize),
+}
+
+impl B3Digest {
+    // constructs a [B3Digest] from a [Vec<u8>].
+    // Returns an error if the digest has the wrong length.
+    pub fn from_vec(value: Vec<u8>) -> Result<Self, Error> {
+        if value.len() != 32 {
+            Err(Error::InvalidDigestLen(value.len()))
+        } else {
+            Ok(Self(value))
+        }
+    }
+
+    // returns a copy of the inner [Vec<u8>].
+    pub fn to_vec(&self) -> Vec<u8> {
+        self.0.to_vec()
+    }
+}
+
+impl From<&[u8; 32]> for B3Digest {
+    fn from(value: &[u8; 32]) -> Self {
+        Self(value.to_vec())
+    }
+}
+
+impl Clone for B3Digest {
+    fn clone(&self) -> Self {
+        Self(self.0.to_owned())
+    }
+}
+
+impl std::fmt::Display for B3Digest {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "b3:{}", BASE64.encode(self.0.as_slice()))
+    }
+}
diff --git a/tvix/store/src/directoryservice/grpc.rs b/tvix/store/src/directoryservice/grpc.rs
new file mode 100644
index 000000000000..1b33572cf7de
--- /dev/null
+++ b/tvix/store/src/directoryservice/grpc.rs
@@ -0,0 +1,532 @@
+use std::collections::HashSet;
+
+use super::{DirectoryPutter, DirectoryService};
+use crate::proto::{self, get_directory_request::ByWhat};
+use crate::{B3Digest, Error};
+use tokio::sync::mpsc::UnboundedSender;
+use tokio_stream::wrappers::UnboundedReceiverStream;
+use tonic::{transport::Channel, Status};
+use tonic::{Code, Streaming};
+use tracing::{instrument, warn};
+
+/// Connects to a (remote) tvix-store DirectoryService over gRPC.
+#[derive(Clone)]
+pub struct GRPCDirectoryService {
+    /// A handle into the active tokio runtime. Necessary to spawn tasks.
+    tokio_handle: tokio::runtime::Handle,
+
+    /// The internal reference to a gRPC client.
+    /// Cloning it is cheap, and it internally handles concurrent requests.
+    grpc_client: proto::directory_service_client::DirectoryServiceClient<Channel>,
+}
+
+impl GRPCDirectoryService {
+    /// Construct a new [GRPCDirectoryService], by passing a handle to the
+    /// tokio runtime, and a gRPC client.
+    pub fn new(
+        tokio_handle: tokio::runtime::Handle,
+        grpc_client: proto::directory_service_client::DirectoryServiceClient<Channel>,
+    ) -> Self {
+        Self {
+            tokio_handle,
+            grpc_client,
+        }
+    }
+
+    /// construct a [GRPCDirectoryService] from a [proto::blob_service_client::BlobServiceClient<Channel>].
+    /// panics if called outside the context of a tokio runtime.
+    pub fn from_client(
+        grpc_client: proto::directory_service_client::DirectoryServiceClient<Channel>,
+    ) -> Self {
+        Self {
+            tokio_handle: tokio::runtime::Handle::current(),
+            grpc_client,
+        }
+    }
+}
+
+impl DirectoryService for GRPCDirectoryService {
+    type DirectoriesIterator = StreamIterator;
+
+    fn get(&self, digest: &B3Digest) -> Result<Option<crate::proto::Directory>, crate::Error> {
+        // Get a new handle to the gRPC client, and copy the digest.
+        let mut grpc_client = self.grpc_client.clone();
+
+        let digest_as_vec = digest.to_vec();
+        let task = self.tokio_handle.spawn(async move {
+            let mut s = grpc_client
+                .get(proto::GetDirectoryRequest {
+                    recursive: false,
+                    by_what: Some(ByWhat::Digest(digest_as_vec)),
+                })
+                .await?
+                .into_inner();
+
+            // Retrieve the first message only, then close the stream (we set recursive to false)
+            s.message().await
+        });
+
+        let digest = digest.clone();
+        match self.tokio_handle.block_on(task)? {
+            Ok(Some(directory)) => {
+                // Validate the retrieved Directory indeed has the
+                // digest we expect it to have, to detect corruptions.
+                let actual_digest = directory.digest();
+                if actual_digest != digest {
+                    Err(crate::Error::StorageError(format!(
+                        "requested directory with digest {}, but got {}",
+                        digest, actual_digest
+                    )))
+                } else if let Err(e) = directory.validate() {
+                    // Validate the Directory itself is valid.
+                    warn!("directory failed validation: {}", e.to_string());
+                    Err(crate::Error::StorageError(format!(
+                        "directory {} failed validation: {}",
+                        digest, e,
+                    )))
+                } else {
+                    Ok(Some(directory))
+                }
+            }
+            Ok(None) => Ok(None),
+            Err(e) if e.code() == Code::NotFound => Ok(None),
+            Err(e) => Err(crate::Error::StorageError(e.to_string())),
+        }
+    }
+
+    fn put(&self, directory: crate::proto::Directory) -> Result<B3Digest, crate::Error> {
+        let mut grpc_client = self.grpc_client.clone();
+
+        let task = self
+            .tokio_handle
+            .spawn(async move { grpc_client.put(tokio_stream::iter(vec![directory])).await });
+
+        match self.tokio_handle.block_on(task)? {
+            Ok(put_directory_resp) => Ok(B3Digest::from_vec(
+                put_directory_resp.into_inner().root_digest,
+            )
+            .map_err(|_| {
+                Error::StorageError("invalid root digest length in response".to_string())
+            })?),
+            Err(e) => Err(crate::Error::StorageError(e.to_string())),
+        }
+    }
+
+    #[instrument(skip_all, fields(directory.digest = %root_directory_digest))]
+    fn get_recursive(&self, root_directory_digest: &B3Digest) -> Self::DirectoriesIterator {
+        let mut grpc_client = self.grpc_client.clone();
+
+        let root_directory_digest_as_vec = root_directory_digest.to_vec();
+        let task: tokio::task::JoinHandle<Result<Streaming<proto::Directory>, Status>> =
+            self.tokio_handle.spawn(async move {
+                let s = grpc_client
+                    .get(proto::GetDirectoryRequest {
+                        recursive: true,
+                        by_what: Some(ByWhat::Digest(root_directory_digest_as_vec)),
+                    })
+                    .await?
+                    .into_inner();
+
+                Ok(s)
+            });
+
+        let stream = self.tokio_handle.block_on(task).unwrap().unwrap();
+
+        StreamIterator::new(
+            self.tokio_handle.clone(),
+            root_directory_digest.clone(),
+            stream,
+        )
+    }
+
+    type DirectoryPutter = GRPCPutter;
+
+    #[instrument(skip_all)]
+    fn put_multiple_start(&self) -> Self::DirectoryPutter
+    where
+        Self: Clone,
+    {
+        let mut grpc_client = self.grpc_client.clone();
+
+        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
+
+        let task: tokio::task::JoinHandle<Result<proto::PutDirectoryResponse, Status>> =
+            self.tokio_handle.spawn(async move {
+                let s = grpc_client
+                    .put(UnboundedReceiverStream::new(rx))
+                    .await?
+                    .into_inner();
+
+                Ok(s)
+            });
+
+        GRPCPutter::new(self.tokio_handle.clone(), tx, task)
+    }
+}
+
+pub struct StreamIterator {
+    /// A handle into the active tokio runtime. Necessary to run futures to completion.
+    tokio_handle: tokio::runtime::Handle,
+    // A stream of [proto::Directory]
+    stream: Streaming<proto::Directory>,
+    // The Directory digests we received so far
+    received_directory_digests: HashSet<B3Digest>,
+    // The Directory digests we're still expecting to get sent.
+    expected_directory_digests: HashSet<B3Digest>,
+}
+
+impl StreamIterator {
+    pub fn new(
+        tokio_handle: tokio::runtime::Handle,
+        root_digest: B3Digest,
+        stream: Streaming<proto::Directory>,
+    ) -> Self {
+        Self {
+            tokio_handle,
+            stream,
+            received_directory_digests: HashSet::new(),
+            expected_directory_digests: HashSet::from([root_digest]),
+        }
+    }
+}
+
+impl Iterator for StreamIterator {
+    type Item = Result<proto::Directory, crate::Error>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self.tokio_handle.block_on(self.stream.message()) {
+            Ok(ok) => match ok {
+                Some(directory) => {
+                    // validate the directory itself.
+                    if let Err(e) = directory.validate() {
+                        return Some(Err(crate::Error::StorageError(format!(
+                            "directory {} failed validation: {}",
+                            directory.digest(),
+                            e,
+                        ))));
+                    }
+                    // validate we actually expected that directory, and move it from expected to received.
+                    let directory_digest = directory.digest();
+                    let was_expected = self.expected_directory_digests.remove(&directory_digest);
+                    if !was_expected {
+                        // FUTUREWORK: dumb clients might send the same stuff twice.
+                        // as a fallback, we might want to tolerate receiving
+                        // it if it's in received_directory_digests (as that
+                        // means it once was in expected_directory_digests)
+                        return Some(Err(crate::Error::StorageError(format!(
+                            "received unexpected directory {}",
+                            directory_digest
+                        ))));
+                    }
+                    self.received_directory_digests.insert(directory_digest);
+
+                    // register all children in expected_directory_digests.
+                    for child_directory in &directory.directories {
+                        // We ran validate() above, so we know these digests must be correct.
+                        let child_directory_digest =
+                            B3Digest::from_vec(child_directory.digest.clone()).unwrap();
+
+                        self.expected_directory_digests
+                            .insert(child_directory_digest);
+                    }
+
+                    Some(Ok(directory))
+                }
+                None => {
+                    // If we were still expecting something, that's an error.
+                    if !self.expected_directory_digests.is_empty() {
+                        Some(Err(crate::Error::StorageError(format!(
+                            "still expected {} directories, but got premature end of stream",
+                            self.expected_directory_digests.len(),
+                        ))))
+                    } else {
+                        None
+                    }
+                }
+            },
+            Err(e) => Some(Err(crate::Error::StorageError(e.to_string()))),
+        }
+    }
+}
+
+/// Allows uploading multiple Directory messages in the same gRPC stream.
+pub struct GRPCPutter {
+    /// A handle into the active tokio runtime. Necessary to spawn tasks.
+    tokio_handle: tokio::runtime::Handle,
+
+    /// Data about the current request - a handle to the task, and the tx part
+    /// of the channel.
+    /// The tx part of the pipe is used to send [proto::Directory] to the ongoing request.
+    /// The task will yield a [proto::PutDirectoryResponse] once the stream is closed.
+    #[allow(clippy::type_complexity)] // lol
+    rq: Option<(
+        tokio::task::JoinHandle<Result<proto::PutDirectoryResponse, Status>>,
+        UnboundedSender<proto::Directory>,
+    )>,
+}
+
+impl GRPCPutter {
+    pub fn new(
+        tokio_handle: tokio::runtime::Handle,
+        directory_sender: UnboundedSender<proto::Directory>,
+        task: tokio::task::JoinHandle<Result<proto::PutDirectoryResponse, Status>>,
+    ) -> Self {
+        Self {
+            tokio_handle,
+            rq: Some((task, directory_sender)),
+        }
+    }
+
+    #[allow(dead_code)]
+    // allows checking if the tx part of the channel is closed.
+    fn is_closed(&self) -> bool {
+        match self.rq {
+            None => true,
+            Some((_, ref directory_sender)) => directory_sender.is_closed(),
+        }
+    }
+}
+
+impl DirectoryPutter for GRPCPutter {
+    fn put(&mut self, directory: proto::Directory) -> Result<(), crate::Error> {
+        match self.rq {
+            // If we're not already closed, send the directory to directory_sender.
+            Some((_, ref directory_sender)) => {
+                if directory_sender.send(directory).is_err() {
+                    // If the channel has been prematurely closed, invoke close (so we can peek at the error code)
+                    // That error code is much more helpful, because it
+                    // contains the error message from the server.
+                    self.close()?;
+                }
+                Ok(())
+            }
+            // If self.close() was already called, we can't put again.
+            None => Err(Error::StorageError(
+                "DirectoryPutter already closed".to_string(),
+            )),
+        }
+    }
+
+    /// Closes the stream for sending, and returns the value
+    fn close(&mut self) -> Result<B3Digest, crate::Error> {
+        // get self.rq, and replace it with None.
+        // This ensures we can only close it once.
+        match std::mem::take(&mut self.rq) {
+            None => Err(Error::StorageError("already closed".to_string())),
+            Some((task, directory_sender)) => {
+                // close directory_sender, so blocking on task will finish.
+                drop(directory_sender);
+
+                let root_digest = self
+                    .tokio_handle
+                    .block_on(task)?
+                    .map_err(|e| Error::StorageError(e.to_string()))?
+                    .root_digest;
+
+                B3Digest::from_vec(root_digest).map_err(|_| {
+                    Error::StorageError("invalid root digest length in response".to_string())
+                })
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use core::time;
+    use std::thread;
+
+    use tempfile::TempDir;
+    use tokio::net::{UnixListener, UnixStream};
+    use tokio_stream::wrappers::UnixListenerStream;
+    use tonic::transport::{Endpoint, Server, Uri};
+
+    use crate::{
+        directoryservice::{DirectoryPutter, DirectoryService},
+        proto,
+        proto::{directory_service_server::DirectoryServiceServer, GRPCDirectoryServiceWrapper},
+        tests::{
+            fixtures::{DIRECTORY_A, DIRECTORY_B},
+            utils::gen_directory_service,
+        },
+    };
+
+    #[test]
+    fn test() -> anyhow::Result<()> {
+        let tmpdir = TempDir::new().unwrap();
+        let socket_path = tmpdir.path().join("socket");
+
+        // Spin up a server, in a thread far away, which spawns its own tokio runtime,
+        // and blocks on the task.
+        let socket_path_clone = socket_path.clone();
+        thread::spawn(move || {
+            // Create the runtime
+            let rt = tokio::runtime::Runtime::new().unwrap();
+            // Get a handle from this runtime
+            let handle = rt.handle();
+
+            let task = handle.spawn(async {
+                let uds = UnixListener::bind(socket_path_clone).unwrap();
+                let uds_stream = UnixListenerStream::new(uds);
+
+                // spin up a new DirectoryService
+                let mut server = Server::builder();
+                let router = server.add_service(DirectoryServiceServer::new(
+                    GRPCDirectoryServiceWrapper::from(gen_directory_service()),
+                ));
+                router.serve_with_incoming(uds_stream).await
+            });
+
+            handle.block_on(task)
+        });
+
+        // set up the local client runtime. This is similar to what the [tokio:test] macro desugars to.
+        let tester_runtime = tokio::runtime::Builder::new_current_thread()
+            .enable_all()
+            .build()
+            .unwrap();
+
+        // wait for the socket to be created
+        {
+            let mut socket_created = false;
+            for _try in 1..20 {
+                if socket_path.exists() {
+                    socket_created = true;
+                    break;
+                }
+                std::thread::sleep(time::Duration::from_millis(20))
+            }
+
+            assert!(
+                socket_created,
+                "expected socket path to eventually get created, but never happened"
+            );
+        }
+
+        let task = tester_runtime.spawn_blocking(move || {
+            // Create a channel, connecting to the uds at socket_path.
+            // The URI is unused.
+            let channel = Endpoint::try_from("http://[::]:50051")
+                .unwrap()
+                .connect_with_connector_lazy(tower::service_fn(move |_: Uri| {
+                    UnixStream::connect(socket_path.clone())
+                }));
+
+            let grpc_client = proto::directory_service_client::DirectoryServiceClient::new(channel);
+
+            // create the GrpcDirectoryService, using the tester_runtime.
+            let directory_service =
+                super::GRPCDirectoryService::new(tokio::runtime::Handle::current(), grpc_client);
+
+            // try to get DIRECTORY_A should return Ok(None)
+            assert_eq!(
+                None,
+                directory_service
+                    .get(&DIRECTORY_A.digest())
+                    .expect("must not fail")
+            );
+
+            // Now upload it
+            assert_eq!(
+                DIRECTORY_A.digest(),
+                directory_service
+                    .put(DIRECTORY_A.clone())
+                    .expect("must succeed")
+            );
+
+            // And retrieve it, compare for equality.
+            assert_eq!(
+                DIRECTORY_A.clone(),
+                directory_service
+                    .get(&DIRECTORY_A.digest())
+                    .expect("must succeed")
+                    .expect("must be some")
+            );
+
+            // Putting DIRECTORY_B alone should fail, because it refers to DIRECTORY_A.
+            directory_service
+                .put(DIRECTORY_B.clone())
+                .expect_err("must fail");
+
+            // Putting DIRECTORY_B in a put_multiple will succeed, but the close
+            // will always fail.
+            {
+                let mut handle = directory_service.put_multiple_start();
+                handle.put(DIRECTORY_B.clone()).expect("must succeed");
+                handle.close().expect_err("must fail");
+            }
+
+            // Uploading A and then B should succeed, and closing should return the digest of B.
+            let mut handle = directory_service.put_multiple_start();
+            handle.put(DIRECTORY_A.clone()).expect("must succeed");
+            handle.put(DIRECTORY_B.clone()).expect("must succeed");
+            let digest = handle.close().expect("must succeed");
+            assert_eq!(DIRECTORY_B.digest(), digest);
+
+            // Now try to retrieve the closure of DIRECTORY_B, which should return B and then A.
+            let mut directories_it = directory_service.get_recursive(&DIRECTORY_B.digest());
+            assert_eq!(
+                DIRECTORY_B.clone(),
+                directories_it
+                    .next()
+                    .expect("must be some")
+                    .expect("must succeed")
+            );
+            assert_eq!(
+                DIRECTORY_A.clone(),
+                directories_it
+                    .next()
+                    .expect("must be some")
+                    .expect("must succeed")
+            );
+
+            // Uploading B and then A should fail, because B refers to A, which
+            // hasn't been uploaded yet.
+            // However, the client can burst, so we might not have received the
+            // error back from the server.
+            {
+                let mut handle = directory_service.put_multiple_start();
+                // sending out B will always be fine
+                handle.put(DIRECTORY_B.clone()).expect("must succeed");
+
+                // whether we will be able to put A as well depends on whether we
+                // already received the error about B.
+                if handle.put(DIRECTORY_A.clone()).is_ok() {
+                    // If we didn't, and this was Ok(_), …
+                    // a subsequent close MUST fail (because it waits for the
+                    // server)
+                    handle.close().expect_err("must fail");
+                }
+            }
+
+            // Now we do the same test as before, send B, then A, but wait
+            // sufficiently enough for the server to have s
+            // to close us the stream,
+            // and then assert that uploading anything else via the handle will fail.
+            {
+                let mut handle = directory_service.put_multiple_start();
+                handle.put(DIRECTORY_B.clone()).expect("must succeed");
+
+                let mut is_closed = false;
+                for _try in 1..1000 {
+                    if handle.is_closed() {
+                        is_closed = true;
+                        break;
+                    }
+                    std::thread::sleep(time::Duration::from_millis(10))
+                }
+
+                assert!(
+                    is_closed,
+                    "expected channel to eventually close, but never happened"
+                );
+
+                handle.put(DIRECTORY_A.clone()).expect_err("must fail");
+            }
+        });
+
+        tester_runtime.block_on(task)?;
+
+        Ok(())
+    }
+}
diff --git a/tvix/store/src/directoryservice/memory.rs b/tvix/store/src/directoryservice/memory.rs
new file mode 100644
index 000000000000..1fd619f7c8cb
--- /dev/null
+++ b/tvix/store/src/directoryservice/memory.rs
@@ -0,0 +1,84 @@
+use crate::{proto, B3Digest, Error};
+use std::collections::HashMap;
+use std::sync::{Arc, RwLock};
+use tracing::{instrument, warn};
+
+use super::utils::SimplePutter;
+use super::{DirectoryService, DirectoryTraverser};
+
+#[derive(Clone, Default)]
+pub struct MemoryDirectoryService {
+    db: Arc<RwLock<HashMap<B3Digest, proto::Directory>>>,
+}
+
+impl DirectoryService for MemoryDirectoryService {
+    type DirectoriesIterator = DirectoryTraverser<Self>;
+
+    #[instrument(skip(self, digest), fields(directory.digest = %digest))]
+    fn get(&self, digest: &B3Digest) -> Result<Option<proto::Directory>, Error> {
+        let db = self.db.read()?;
+
+        match db.get(digest) {
+            // The directory was not found, return
+            None => Ok(None),
+
+            // The directory was found, try to parse the data as Directory message
+            Some(directory) => {
+                // Validate the retrieved Directory indeed has the
+                // digest we expect it to have, to detect corruptions.
+                let actual_digest = directory.digest();
+                if actual_digest != *digest {
+                    return Err(Error::StorageError(format!(
+                        "requested directory with digest {}, but got {}",
+                        digest, actual_digest
+                    )));
+                }
+
+                // Validate the Directory itself is valid.
+                if let Err(e) = directory.validate() {
+                    warn!("directory failed validation: {}", e.to_string());
+                    return Err(Error::StorageError(format!(
+                        "directory {} failed validation: {}",
+                        actual_digest, e,
+                    )));
+                }
+
+                Ok(Some(directory.clone()))
+            }
+        }
+    }
+
+    #[instrument(skip(self, directory), fields(directory.digest = %directory.digest()))]
+    fn put(&self, directory: proto::Directory) -> Result<B3Digest, Error> {
+        let digest = directory.digest();
+
+        // validate the directory itself.
+        if let Err(e) = directory.validate() {
+            return Err(Error::InvalidRequest(format!(
+                "directory {} failed validation: {}",
+                digest, e,
+            )));
+        }
+
+        // store it
+        let mut db = self.db.write()?;
+        db.insert(digest.clone(), directory);
+
+        Ok(digest)
+    }
+
+    #[instrument(skip_all, fields(directory.digest = %root_directory_digest))]
+    fn get_recursive(&self, root_directory_digest: &B3Digest) -> Self::DirectoriesIterator {
+        DirectoryTraverser::with(self.clone(), root_directory_digest)
+    }
+
+    type DirectoryPutter = SimplePutter<Self>;
+
+    #[instrument(skip_all)]
+    fn put_multiple_start(&self) -> Self::DirectoryPutter
+    where
+        Self: Clone,
+    {
+        SimplePutter::new(self.clone())
+    }
+}
diff --git a/tvix/store/src/directoryservice/mod.rs b/tvix/store/src/directoryservice/mod.rs
new file mode 100644
index 000000000000..f387d28948f0
--- /dev/null
+++ b/tvix/store/src/directoryservice/mod.rs
@@ -0,0 +1,54 @@
+use crate::{proto, B3Digest, Error};
+mod grpc;
+mod memory;
+mod sled;
+mod traverse;
+mod utils;
+
+pub use self::grpc::GRPCDirectoryService;
+pub use self::memory::MemoryDirectoryService;
+pub use self::sled::SledDirectoryService;
+pub use self::traverse::traverse_to;
+pub use self::utils::DirectoryTraverser;
+
+/// The base trait all Directory services need to implement.
+/// This is a simple get and put of [crate::proto::Directory], returning their
+/// digest.
+pub trait DirectoryService {
+    type DirectoriesIterator: Iterator<Item = Result<proto::Directory, Error>> + Send;
+    type DirectoryPutter: DirectoryPutter;
+
+    /// Get looks up a single Directory message by its digest.
+    /// In case the directory is not found, Ok(None) is returned.
+    fn get(&self, digest: &B3Digest) -> Result<Option<proto::Directory>, Error>;
+    /// Get uploads a single Directory message, and returns the calculated
+    /// digest, or an error.
+    fn put(&self, directory: proto::Directory) -> Result<B3Digest, Error>;
+
+    /// Looks up a closure of [proto::Directory].
+    /// Ideally this would be a `impl Iterator<Item = Result<proto::Directory, Error>>`,
+    /// and we'd be able to add a default implementation for it here, but
+    /// we can't have that yet.
+    fn get_recursive(&self, root_directory_digest: &B3Digest) -> Self::DirectoriesIterator;
+
+    /// Allows persisting a closure of [proto::Directory], which is a graph of
+    /// connected Directory messages.
+    fn put_multiple_start(&self) -> Self::DirectoryPutter;
+}
+
+/// Provides a handle to put a closure of connected [proto::Directory] elements.
+///
+/// The consumer can periodically call [put], starting from the leaves. Once
+/// the root is reached, [close] can be called to retrieve the root digest (or
+/// an error).
+pub trait DirectoryPutter {
+    /// Put a individual [proto::Directory] into the store.
+    /// Error semantics and behaviour is up to the specific implementation of
+    /// this trait.
+    /// Due to bursting, the returned error might refer to an object previously
+    /// sent via `put`.
+    fn put(&mut self, directory: proto::Directory) -> Result<(), Error>;
+
+    /// Close the stream, and wait for any errors.
+    fn close(&mut self) -> Result<B3Digest, Error>;
+}
diff --git a/tvix/store/src/directoryservice/sled.rs b/tvix/store/src/directoryservice/sled.rs
new file mode 100644
index 000000000000..e189e8acf507
--- /dev/null
+++ b/tvix/store/src/directoryservice/sled.rs
@@ -0,0 +1,107 @@
+use crate::proto::Directory;
+use crate::{proto, B3Digest, Error};
+use prost::Message;
+use std::path::PathBuf;
+use tracing::{instrument, warn};
+
+use super::utils::SimplePutter;
+use super::{DirectoryService, DirectoryTraverser};
+
+#[derive(Clone)]
+pub struct SledDirectoryService {
+    db: sled::Db,
+}
+
+impl SledDirectoryService {
+    pub fn new(p: PathBuf) -> Result<Self, sled::Error> {
+        let config = sled::Config::default().use_compression(true).path(p);
+        let db = config.open()?;
+
+        Ok(Self { db })
+    }
+
+    pub fn new_temporary() -> Result<Self, sled::Error> {
+        let config = sled::Config::default().temporary(true);
+        let db = config.open()?;
+
+        Ok(Self { db })
+    }
+}
+
+impl DirectoryService for SledDirectoryService {
+    type DirectoriesIterator = DirectoryTraverser<Self>;
+
+    #[instrument(skip(self, digest), fields(directory.digest = %digest))]
+    fn get(&self, digest: &B3Digest) -> Result<Option<proto::Directory>, Error> {
+        match self.db.get(digest.to_vec()) {
+            // The directory was not found, return
+            Ok(None) => Ok(None),
+
+            // The directory was found, try to parse the data as Directory message
+            Ok(Some(data)) => match Directory::decode(&*data) {
+                Ok(directory) => {
+                    // Validate the retrieved Directory indeed has the
+                    // digest we expect it to have, to detect corruptions.
+                    let actual_digest = directory.digest();
+                    if actual_digest != *digest {
+                        return Err(Error::StorageError(format!(
+                            "requested directory with digest {}, but got {}",
+                            digest, actual_digest
+                        )));
+                    }
+
+                    // Validate the Directory itself is valid.
+                    if let Err(e) = directory.validate() {
+                        warn!("directory failed validation: {}", e.to_string());
+                        return Err(Error::StorageError(format!(
+                            "directory {} failed validation: {}",
+                            actual_digest, e,
+                        )));
+                    }
+
+                    Ok(Some(directory))
+                }
+                Err(e) => {
+                    warn!("unable to parse directory {}: {}", digest, e);
+                    Err(Error::StorageError(e.to_string()))
+                }
+            },
+            // some storage error?
+            Err(e) => Err(Error::StorageError(e.to_string())),
+        }
+    }
+
+    #[instrument(skip(self, directory), fields(directory.digest = %directory.digest()))]
+    fn put(&self, directory: proto::Directory) -> Result<B3Digest, Error> {
+        let digest = directory.digest();
+
+        // validate the directory itself.
+        if let Err(e) = directory.validate() {
+            return Err(Error::InvalidRequest(format!(
+                "directory {} failed validation: {}",
+                digest, e,
+            )));
+        }
+        // store it
+        let result = self.db.insert(digest.to_vec(), directory.encode_to_vec());
+        if let Err(e) = result {
+            return Err(Error::StorageError(e.to_string()));
+        }
+        Ok(digest)
+    }
+
+    #[instrument(skip_all, fields(directory.digest = %root_directory_digest))]
+    fn get_recursive(&self, root_directory_digest: &B3Digest) -> Self::DirectoriesIterator {
+        DirectoryTraverser::with(self.clone(), root_directory_digest)
+    }
+
+    type DirectoryPutter = SimplePutter<Self>;
+
+    #[instrument(skip_all)]
+    fn put_multiple_start(&self) -> Self::DirectoryPutter
+    where
+        Self: Clone,
+    {
+        SimplePutter::new(self.clone())
+    }
+}
diff --git a/tvix/store/src/directoryservice/traverse.rs b/tvix/store/src/directoryservice/traverse.rs
new file mode 100644
index 000000000000..8691baa8b73f
--- /dev/null
+++ b/tvix/store/src/directoryservice/traverse.rs
@@ -0,0 +1,222 @@
+use super::DirectoryService;
+use crate::{proto::NamedNode, B3Digest, Error};
+use tracing::{instrument, warn};
+
+/// This traverses from a (root) node to the given (sub)path, returning the Node
+/// at that path, or none, if there's nothing at that path.
+/// TODO: Do we want to rewrite this in a non-recursing fashion, and use
+/// [DirectoryService.get_recursive] to do less lookups?
+/// Or do we consider this to be a non-issue due to store composition and local caching?
+/// TODO: the name of this function (and mod) is a bit bad, because it doesn't
+/// clearly distinguish it from the BFS traversers.
+#[instrument(skip(directory_service))]
+pub fn traverse_to<DS: DirectoryService>(
+    directory_service: &DS,
+    node: crate::proto::node::Node,
+    path: &std::path::Path,
+) -> Result<Option<crate::proto::node::Node>, Error> {
+    // strip a possible `/` prefix from the path.
+    let path = {
+        if path.starts_with("/") {
+            path.strip_prefix("/").unwrap()
+        } else {
+            path
+        }
+    };
+
+    let mut it = path.components();
+
+    match it.next() {
+        None => {
+            // the (remaining) path is empty, return the node we've been called with.
+            Ok(Some(node))
+        }
+        Some(first_component) => {
+            match node {
+                crate::proto::node::Node::File(_) | crate::proto::node::Node::Symlink(_) => {
+                    // There's still some path left, but the current node is no directory.
+                    // This means the path doesn't exist, as we can't reach it.
+                    Ok(None)
+                }
+                crate::proto::node::Node::Directory(directory_node) => {
+                    let digest = B3Digest::from_vec(directory_node.digest)
+                        .map_err(|_e| Error::StorageError("invalid digest length".to_string()))?;
+
+                    // fetch the linked node from the directory_service
+                    match directory_service.get(&digest)? {
+                        // If we didn't get the directory node that's linked, that's a store inconsistency, bail out!
+                        None => {
+                            warn!("directory {} does not exist", digest);
+
+                            Err(Error::StorageError(format!(
+                                "directory {} does not exist",
+                                digest
+                            )))
+                        }
+                        Some(directory) => {
+                            // look for first_component in the [Directory].
+                            // FUTUREWORK: as the nodes() iterator returns in a sorted fashion, we
+                            // could stop as soon as e.name is larger than the search string.
+                            let child_node = directory.nodes().find(|n| {
+                                n.get_name() == first_component.as_os_str().to_str().unwrap()
+                            });
+
+                            match child_node {
+                                // child node not found means there's no such element inside the directory.
+                                None => Ok(None),
+                                // child node found, recurse with it and the rest of the path.
+                                Some(child_node) => {
+                                    let rest_path: std::path::PathBuf = it.collect();
+                                    traverse_to(directory_service, child_node, &rest_path)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::path::PathBuf;
+
+    use crate::{
+        directoryservice::DirectoryPutter,
+        directoryservice::DirectoryService,
+        tests::{
+            fixtures::{DIRECTORY_COMPLICATED, DIRECTORY_WITH_KEEP},
+            utils::gen_directory_service,
+        },
+    };
+
+    use super::traverse_to;
+
+    #[test]
+    fn test_traverse_to() {
+        let mut directory_service = gen_directory_service();
+
+        let mut handle = directory_service.put_multiple_start();
+        handle
+            .put(DIRECTORY_WITH_KEEP.clone())
+            .expect("must succeed");
+        handle
+            .put(DIRECTORY_COMPLICATED.clone())
+            .expect("must succeed");
+
+        // construct the node for DIRECTORY_COMPLICATED
+        let node_directory_complicated =
+            crate::proto::node::Node::Directory(crate::proto::DirectoryNode {
+                name: "doesntmatter".to_string(),
+                digest: DIRECTORY_COMPLICATED.digest().to_vec(),
+                size: DIRECTORY_COMPLICATED.size(),
+            });
+
+        // construct the node for DIRECTORY_COMPLICATED
+        let node_directory_with_keep = crate::proto::node::Node::Directory(
+            DIRECTORY_COMPLICATED.directories.first().unwrap().clone(),
+        );
+
+        // construct the node for the .keep file
+        let node_file_keep =
+            crate::proto::node::Node::File(DIRECTORY_WITH_KEEP.files.first().unwrap().clone());
+
+        // traversal to an empty subpath should return the root node.
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from(""),
+            )
+            .expect("must succeed");
+
+            assert_eq!(Some(node_directory_complicated.clone()), resp);
+        }
+
+        // traversal to `keep` should return the node for DIRECTORY_WITH_KEEP
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from("keep"),
+            )
+            .expect("must succeed");
+
+            assert_eq!(Some(node_directory_with_keep.clone()), resp);
+        }
+
+        // traversal to `keep/.keep` should return the node for the .keep file
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from("keep/.keep"),
+            )
+            .expect("must succeed");
+
+            assert_eq!(Some(node_file_keep.clone()), resp);
+        }
+
+        // traversal to `keep/.keep` should return the node for the .keep file
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from("/keep/.keep"),
+            )
+            .expect("must succeed");
+
+            assert_eq!(Some(node_file_keep.clone()), resp);
+        }
+
+        // traversal to `void` should return None (doesn't exist)
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from("void"),
+            )
+            .expect("must succeed");
+
+            assert_eq!(None, resp);
+        }
+
+        // traversal to `void` should return None (doesn't exist)
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from("//v/oid"),
+            )
+            .expect("must succeed");
+
+            assert_eq!(None, resp);
+        }
+
+        // traversal to `keep/.keep/404` should return None (the path can't be
+        // reached, as keep/.keep already is a file)
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from("keep/.keep/foo"),
+            )
+            .expect("must succeed");
+
+            assert_eq!(None, resp);
+        }
+
+        // traversal to a subpath of '/' should return the root node.
+        {
+            let resp = traverse_to(
+                &mut directory_service,
+                node_directory_complicated.clone(),
+                &PathBuf::from("/"),
+            )
+            .expect("must succeed");
+
+            assert_eq!(Some(node_directory_complicated.clone()), resp);
+        }
+    }
+}
diff --git a/tvix/store/src/directoryservice/utils.rs b/tvix/store/src/directoryservice/utils.rs
new file mode 100644
index 000000000000..3661808734f3
--- /dev/null
+++ b/tvix/store/src/directoryservice/utils.rs
@@ -0,0 +1,140 @@
+use super::DirectoryPutter;
+use super::DirectoryService;
+use crate::proto;
+use crate::B3Digest;
+use crate::Error;
+use std::collections::{HashSet, VecDeque};
+use tracing::{debug_span, instrument, warn};
+
+/// Traverses a [proto::Directory] from the root to the children.
+///
+/// This is mostly BFS, but directories are only returned once.
+pub struct DirectoryTraverser<DS: DirectoryService> {
+    directory_service: DS,
+    /// The list of all directories that still need to be traversed. The next
+    /// element is picked from the front, new elements are enqueued at the
+    /// back.
+    worklist_directory_digests: VecDeque<B3Digest>,
+    /// The list of directory digests already sent to the consumer.
+    /// We omit sending the same directories multiple times.
+    sent_directory_digests: HashSet<B3Digest>,
+}
+
+impl<DS: DirectoryService> DirectoryTraverser<DS> {
+    pub fn with(directory_service: DS, root_directory_digest: &B3Digest) -> Self {
+        Self {
+            directory_service,
+            worklist_directory_digests: VecDeque::from([root_directory_digest.clone()]),
+            sent_directory_digests: HashSet::new(),
+        }
+    }
+
+    // enqueue all child directory digests to the work queue, as
+    // long as they're not part of the worklist or already sent.
+    // This panics if the digest looks invalid, it's supposed to be checked first.
+    fn enqueue_child_directories(&mut self, directory: &proto::Directory) {
+        for child_directory_node in &directory.directories {
+            // TODO: propagate error
+            let child_digest = B3Digest::from_vec(child_directory_node.digest.clone()).unwrap();
+
+            if self.worklist_directory_digests.contains(&child_digest)
+                || self.sent_directory_digests.contains(&child_digest)
+            {
+                continue;
+            }
+            self.worklist_directory_digests.push_back(child_digest);
+        }
+    }
+}
+
+impl<DS: DirectoryService> Iterator for DirectoryTraverser<DS> {
+    type Item = Result<proto::Directory, Error>;
+
+    #[instrument(skip_all)]
+    fn next(&mut self) -> Option<Self::Item> {
+        // fetch the next directory digest from the top of the work queue.
+        match self.worklist_directory_digests.pop_front() {
+            None => None,
+            Some(current_directory_digest) => {
+                let span = debug_span!("directory.digest", "{}", current_directory_digest);
+                let _ = span.enter();
+
+                // look up the directory itself.
+                let current_directory = match self.directory_service.get(&current_directory_digest)
+                {
+                    // if we got it
+                    Ok(Some(current_directory)) => {
+                        // validate, we don't want to send invalid directories.
+                        if let Err(e) = current_directory.validate() {
+                            warn!("directory failed validation: {}", e.to_string());
+                            return Some(Err(Error::StorageError(format!(
+                                "invalid directory: {}",
+                                current_directory_digest
+                            ))));
+                        }
+                        current_directory
+                    }
+                    // if it's not there, we have an inconsistent store!
+                    Ok(None) => {
+                        warn!("directory {} does not exist", current_directory_digest);
+                        return Some(Err(Error::StorageError(format!(
+                            "directory {} does not exist",
+                            current_directory_digest
+                        ))));
+                    }
+                    Err(e) => {
+                        warn!("failed to look up directory");
+                        return Some(Err(Error::StorageError(format!(
+                            "unable to look up directory {}: {}",
+                            current_directory_digest, e
+                        ))));
+                    }
+                };
+
+                // All DirectoryServices MUST validate directory nodes, before returning them out, so we
+                // can be sure [enqueue_child_directories] doesn't panic.
+
+                // enqueue child directories
+                self.enqueue_child_directories(&current_directory);
+                Some(Ok(current_directory))
+            }
+        }
+    }
+}
+
+/// This is a simple implementation of a Directory uploader.
+/// TODO: verify connectivity? Factor out these checks into generic helpers?
+pub struct SimplePutter<DS: DirectoryService> {
+    directory_service: DS,
+    last_directory_digest: Option<B3Digest>,
+}
+
+impl<DS: DirectoryService> SimplePutter<DS> {
+    pub fn new(directory_service: DS) -> Self {
+        Self {
+            directory_service,
+            last_directory_digest: None,
+        }
+    }
+}
+
+impl<DS: DirectoryService> DirectoryPutter for SimplePutter<DS> {
+    fn put(&mut self, directory: proto::Directory) -> Result<(), Error> {
+        let digest = self.directory_service.put(directory)?;
+
+        // track the last directory digest
+        self.last_directory_digest = Some(digest);
+
+        Ok(())
+    }
+
+    /// We need to be mutable here, as that's the signature of the trait.
+    fn close(&mut self) -> Result<B3Digest, Error> {
+        match &self.last_directory_digest {
+            Some(last_digest) => Ok(last_digest.clone()),
+            None => Err(Error::InvalidRequest(
+                "no directories sent, can't show root digest".to_string(),
+            )),
+        }
+    }
+}
diff --git a/tvix/store/src/errors.rs b/tvix/store/src/errors.rs
new file mode 100644
index 000000000000..3b23f972b045
--- /dev/null
+++ b/tvix/store/src/errors.rs
@@ -0,0 +1,45 @@
+use std::sync::PoisonError;
+use thiserror::Error;
+use tokio::task::JoinError;
+use tonic::Status;
+
+/// Errors related to communication with the store.
+#[derive(Debug, Error)]
+pub enum Error {
+    #[error("invalid request: {0}")]
+    InvalidRequest(String),
+
+    #[error("internal storage error: {0}")]
+    StorageError(String),
+}
+
+impl<T> From<PoisonError<T>> for Error {
+    fn from(value: PoisonError<T>) -> Self {
+        Error::StorageError(value.to_string())
+    }
+}
+
+impl From<JoinError> for Error {
+    fn from(value: JoinError) -> Self {
+        Error::StorageError(value.to_string())
+    }
+}
+
+impl From<Error> for Status {
+    fn from(value: Error) -> Self {
+        match value {
+            Error::InvalidRequest(msg) => Status::invalid_argument(msg),
+            Error::StorageError(msg) => Status::data_loss(format!("storage error: {}", msg)),
+        }
+    }
+}
+
+// TODO: this should probably go somewhere else?
+impl From<Error> for std::io::Error {
+    fn from(value: Error) -> Self {
+        match value {
+            Error::InvalidRequest(msg) => Self::new(std::io::ErrorKind::InvalidInput, msg),
+            Error::StorageError(msg) => Self::new(std::io::ErrorKind::Other, msg),
+        }
+    }
+}
diff --git a/tvix/store/src/import.rs b/tvix/store/src/import.rs
new file mode 100644
index 000000000000..206e5eaba975
--- /dev/null
+++ b/tvix/store/src/import.rs
@@ -0,0 +1,220 @@
+use crate::{blobservice::BlobService, directoryservice::DirectoryService};
+use crate::{blobservice::BlobWriter, directoryservice::DirectoryPutter, proto};
+use std::{
+    collections::HashMap,
+    fmt::Debug,
+    fs,
+    fs::File,
+    io,
+    os::unix::prelude::PermissionsExt,
+    path::{Path, PathBuf},
+};
+use tracing::instrument;
+use walkdir::WalkDir;
+
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+    #[error("failed to upload directory at {0}: {1}")]
+    UploadDirectoryError(PathBuf, crate::Error),
+
+    #[error("invalid encoding encountered for entry {0:?}")]
+    InvalidEncoding(PathBuf),
+
+    #[error("unable to stat {0}: {1}")]
+    UnableToStat(PathBuf, std::io::Error),
+
+    #[error("unable to open {0}: {1}")]
+    UnableToOpen(PathBuf, std::io::Error),
+
+    #[error("unable to read {0}: {1}")]
+    UnableToRead(PathBuf, std::io::Error),
+}
+
+impl From<super::Error> for Error {
+    fn from(value: super::Error) -> Self {
+        match value {
+            crate::Error::InvalidRequest(_) => panic!("tvix bug"),
+            crate::Error::StorageError(_) => panic!("error"),
+        }
+    }
+}
+
+// This processes a given [walkdir::DirEntry] and returns a
+// proto::node::Node, depending on the type of the entry.
+//
+// If the entry is a file, its contents are uploaded.
+// If the entry is a directory, the Directory is uploaded as well.
+// For this to work, it relies on the caller to provide the directory object
+// with the previously returned (child) nodes.
+//
+// It assumes entries to be returned in "contents first" order, means this
+// will only be called with a directory if all children of it have been
+// visited. If the entry is indeed a directory, it'll also upload that
+// directory to the store. For this, the so-far-assembled Directory object for
+// this path needs to be passed in.
+//
+// It assumes the caller adds returned nodes to the directories it assembles.
+#[instrument(skip_all, fields(entry.file_type=?&entry.file_type(),entry.path=?entry.path()))]
+fn process_entry<BS: BlobService, DP: DirectoryPutter>(
+    blob_service: &BS,
+    directory_putter: &mut DP,
+    entry: &walkdir::DirEntry,
+    maybe_directory: Option<proto::Directory>,
+) -> Result<proto::node::Node, Error> {
+    let file_type = entry.file_type();
+
+    let entry_path: PathBuf = entry.path().to_path_buf();
+
+    if file_type.is_dir() {
+        let directory = maybe_directory
+            .expect("tvix bug: must be called with some directory in the case of directory");
+        let directory_digest = directory.digest();
+        let directory_size = directory.size();
+
+        // upload this directory
+        directory_putter
+            .put(directory)
+            .map_err(|e| Error::UploadDirectoryError(entry.path().to_path_buf(), e))?;
+
+        return Ok(proto::node::Node::Directory(proto::DirectoryNode {
+            name: entry
+                .file_name()
+                .to_str()
+                .map(|s| Ok(s.to_owned()))
+                .unwrap_or(Err(Error::InvalidEncoding(entry.path().to_path_buf())))?,
+            digest: directory_digest.to_vec(),
+            size: directory_size,
+        }));
+    }
+
+    if file_type.is_symlink() {
+        let target = std::fs::read_link(&entry_path)
+            .map_err(|e| Error::UnableToStat(entry_path.clone(), e))?;
+
+        return Ok(proto::node::Node::Symlink(proto::SymlinkNode {
+            name: entry
+                .file_name()
+                .to_str()
+                .map(|s| Ok(s.to_owned()))
+                .unwrap_or(Err(Error::InvalidEncoding(entry.path().to_path_buf())))?,
+            target: target
+                .to_str()
+                .map(|s| Ok(s.to_owned()))
+                .unwrap_or(Err(Error::InvalidEncoding(entry.path().to_path_buf())))?,
+        }));
+    }
+
+    if file_type.is_file() {
+        let metadata = entry
+            .metadata()
+            .map_err(|e| Error::UnableToStat(entry_path.clone(), e.into()))?;
+
+        let mut file = File::open(entry_path.clone())
+            .map_err(|e| Error::UnableToOpen(entry_path.clone(), e))?;
+
+        let mut writer = blob_service.open_write()?;
+
+        if let Err(e) = io::copy(&mut file, &mut writer) {
+            return Err(Error::UnableToRead(entry_path, e));
+        };
+
+        let digest = writer.close()?;
+
+        return Ok(proto::node::Node::File(proto::FileNode {
+            name: entry
+                .file_name()
+                .to_str()
+                .map(|s| Ok(s.to_owned()))
+                .unwrap_or(Err(Error::InvalidEncoding(entry.path().to_path_buf())))?,
+            digest: digest.to_vec(),
+            size: metadata.len() as u32,
+            // If it's executable by the user, it'll become executable.
+            // This matches nix's dump() function behaviour.
+            executable: metadata.permissions().mode() & 64 != 0,
+        }));
+    }
+    todo!("handle other types")
+}
+
+/// Ingests the contents at the given path into the tvix store,
+/// interacting with a [BlobService] and [DirectoryService].
+/// It returns the root node or an error.
+///
+/// It's not interacting with a [PathInfoService], it's up to the caller to
+/// possibly register it somewhere (and potentially rename it based on some
+/// naming scheme.
+#[instrument(skip(blob_service, directory_service), fields(path=?p))]
+pub fn ingest_path<BS: BlobService, DS: DirectoryService, P: AsRef<Path> + Debug>(
+    blob_service: &BS,
+    directory_service: &DS,
+    p: P,
+) -> Result<proto::node::Node, Error> {
+    // Probe if the path points to a symlink. If it does, we process it manually,
+    // due to https://github.com/BurntSushi/walkdir/issues/175.
+    let symlink_metadata = fs::symlink_metadata(p.as_ref())
+        .map_err(|e| Error::UnableToStat(p.as_ref().to_path_buf(), e))?;
+    if symlink_metadata.is_symlink() {
+        let target = std::fs::read_link(p.as_ref())
+            .map_err(|e| Error::UnableToStat(p.as_ref().to_path_buf(), e))?;
+        return Ok(proto::node::Node::Symlink(proto::SymlinkNode {
+            name: p
+                .as_ref()
+                .file_name()
+                .unwrap_or_default()
+                .to_str()
+                .map(|s| Ok(s.to_owned()))
+                .unwrap_or(Err(Error::InvalidEncoding(p.as_ref().to_path_buf())))?,
+            target: target
+                .to_str()
+                .map(|s| Ok(s.to_owned()))
+                .unwrap_or(Err(Error::InvalidEncoding(p.as_ref().to_path_buf())))?,
+        }));
+    }
+
+    let mut directories: HashMap<PathBuf, proto::Directory> = HashMap::default();
+
+    let mut directory_putter = directory_service.put_multiple_start();
+
+    for entry in WalkDir::new(p)
+        .follow_links(false)
+        .contents_first(true)
+        .sort_by_file_name()
+    {
+        let entry = entry.unwrap();
+
+        // process_entry wants an Option<Directory> in case the entry points to a directory.
+        // make sure to provide it.
+        let maybe_directory: Option<proto::Directory> = {
+            if entry.file_type().is_dir() {
+                Some(
+                    directories
+                        .entry(entry.path().to_path_buf())
+                        .or_default()
+                        .clone(),
+                )
+            } else {
+                None
+            }
+        };
+
+        let node = process_entry(blob_service, &mut directory_putter, &entry, maybe_directory)?;
+
+        if entry.depth() == 0 {
+            return Ok(node);
+        } else {
+            // calculate the parent path, and make sure we register the node there.
+            // NOTE: entry.depth() > 0
+            let parent_path = entry.path().parent().unwrap().to_path_buf();
+
+            // record node in parent directory, creating a new [proto:Directory] if not there yet.
+            let parent_directory = directories.entry(parent_path).or_default();
+            match node {
+                proto::node::Node::Directory(e) => parent_directory.directories.push(e),
+                proto::node::Node::File(e) => parent_directory.files.push(e),
+                proto::node::Node::Symlink(e) => parent_directory.symlinks.push(e),
+            }
+        }
+    }
+    // unreachable, we already bailed out before if root doesn't exist.
+    panic!("tvix bug")
+}
diff --git a/tvix/store/src/lib.rs b/tvix/store/src/lib.rs
new file mode 100644
index 000000000000..7ae8587f8b26
--- /dev/null
+++ b/tvix/store/src/lib.rs
@@ -0,0 +1,17 @@
+mod digests;
+mod errors;
+mod store_io;
+
+pub mod blobservice;
+pub mod directoryservice;
+pub mod import;
+pub mod nar;
+pub mod pathinfoservice;
+pub mod proto;
+
+pub use digests::B3Digest;
+pub use errors::Error;
+pub use store_io::TvixStoreIO;
+
+#[cfg(test)]
+mod tests;
diff --git a/tvix/store/src/nar/grpc_nar_calculation_service.rs b/tvix/store/src/nar/grpc_nar_calculation_service.rs
new file mode 100644
index 000000000000..429593743914
--- /dev/null
+++ b/tvix/store/src/nar/grpc_nar_calculation_service.rs
@@ -0,0 +1,69 @@
+use super::NARCalculationService;
+use crate::proto;
+use tonic::transport::Channel;
+use tonic::Status;
+
+/// A NAR calculation service which asks a remote tvix-store for NAR calculation
+/// (via the gRPC PathInfoService).
+#[derive(Clone)]
+pub struct GRPCNARCalculationService {
+    /// A handle into the active tokio runtime. Necessary to spawn tasks.
+    tokio_handle: tokio::runtime::Handle,
+
+    /// The internal reference to a gRPC client.
+    /// Cloning it is cheap, and it internally handles concurrent requests.
+    grpc_client: proto::path_info_service_client::PathInfoServiceClient<Channel>,
+}
+
+impl GRPCNARCalculationService {
+    /// construct a new [GRPCNARCalculationService], by passing a handle to the
+    /// tokio runtime, and a gRPC client.
+    pub fn new(
+        tokio_handle: tokio::runtime::Handle,
+        grpc_client: proto::path_info_service_client::PathInfoServiceClient<Channel>,
+    ) -> Self {
+        Self {
+            tokio_handle,
+            grpc_client,
+        }
+    }
+
+    /// construct a [GRPCNARCalculationService], from a [proto::path_info_service_client::PathInfoServiceClient<Channel>].
+    /// panics if called outside the context of a tokio runtime.
+    pub fn from_client(
+        grpc_client: proto::path_info_service_client::PathInfoServiceClient<Channel>,
+    ) -> Self {
+        Self {
+            tokio_handle: tokio::runtime::Handle::current(),
+            grpc_client,
+        }
+    }
+}
+
+impl NARCalculationService for GRPCNARCalculationService {
+    fn calculate_nar(
+        &self,
+        root_node: &proto::node::Node,
+    ) -> Result<(u64, [u8; 32]), super::RenderError> {
+        // Get a new handle to the gRPC client, and copy the root node.
+        let mut grpc_client = self.grpc_client.clone();
+        let root_node = root_node.clone();
+
+        let task: tokio::task::JoinHandle<Result<_, Status>> =
+            self.tokio_handle.spawn(async move {
+                Ok(grpc_client
+                    .calculate_nar(proto::Node {
+                        node: Some(root_node),
+                    })
+                    .await?
+                    .into_inner())
+            });
+
+        match self.tokio_handle.block_on(task).unwrap() {
+            Ok(resp) => Ok((resp.nar_size, resp.nar_sha256.to_vec().try_into().unwrap())),
+            Err(e) => Err(super::RenderError::StoreError(crate::Error::StorageError(
+                e.to_string(),
+            ))),
+        }
+    }
+}
diff --git a/tvix/store/src/nar/mod.rs b/tvix/store/src/nar/mod.rs
new file mode 100644
index 000000000000..a29cc5451bae
--- /dev/null
+++ b/tvix/store/src/nar/mod.rs
@@ -0,0 +1,35 @@
+use crate::{proto, B3Digest};
+use data_encoding::BASE64;
+use thiserror::Error;
+
+mod grpc_nar_calculation_service;
+mod non_caching_calculation_service;
+mod renderer;
+
+pub use grpc_nar_calculation_service::GRPCNARCalculationService;
+pub use non_caching_calculation_service::NonCachingNARCalculationService;
+pub use renderer::NARRenderer;
+
+/// Errors that can encounter while rendering NARs.
+#[derive(Debug, Error)]
+pub enum RenderError {
+    #[error("failure talking to a backing store client: {0}")]
+    StoreError(crate::Error),
+
+    #[error("unable to find directory {}, referred from {}", .0, .1)]
+    DirectoryNotFound(B3Digest, String),
+
+    #[error("unable to find blob {}, referred from {}", BASE64.encode(.0), .1)]
+    BlobNotFound([u8; 32], String),
+
+    #[error("unexpected size in metadata for blob {}, referred from {} returned, expected {}, got {}", BASE64.encode(.0), .1, .2, .3)]
+    UnexpectedBlobMeta([u8; 32], String, u32, u32),
+
+    #[error("failure using the NAR writer: {0}")]
+    NARWriterError(std::io::Error),
+}
+
+/// The base trait for something calculating NARs, and returning their size and sha256.
+pub trait NARCalculationService {
+    fn calculate_nar(&self, root_node: &proto::node::Node) -> Result<(u64, [u8; 32]), RenderError>;
+}
diff --git a/tvix/store/src/nar/non_caching_calculation_service.rs b/tvix/store/src/nar/non_caching_calculation_service.rs
new file mode 100644
index 000000000000..8a080cb4df5e
--- /dev/null
+++ b/tvix/store/src/nar/non_caching_calculation_service.rs
@@ -0,0 +1,37 @@
+use count_write::CountWrite;
+use sha2::{Digest, Sha256};
+
+use crate::blobservice::BlobService;
+use crate::directoryservice::DirectoryService;
+use crate::proto;
+
+use super::renderer::NARRenderer;
+use super::{NARCalculationService, RenderError};
+
+/// A NAR calculation service which simply renders the whole NAR whenever
+/// we ask for the calculation.
+#[derive(Clone)]
+pub struct NonCachingNARCalculationService<BS: BlobService, DS: DirectoryService> {
+    nar_renderer: NARRenderer<BS, DS>,
+}
+
+impl<BS: BlobService, DS: DirectoryService> NonCachingNARCalculationService<BS, DS> {
+    pub fn new(blob_service: BS, directory_service: DS) -> Self {
+        Self {
+            nar_renderer: NARRenderer::new(blob_service, directory_service),
+        }
+    }
+}
+
+impl<BS: BlobService, DS: DirectoryService> NARCalculationService
+    for NonCachingNARCalculationService<BS, DS>
+{
+    fn calculate_nar(&self, root_node: &proto::node::Node) -> Result<(u64, [u8; 32]), RenderError> {
+        let h = Sha256::new();
+        let mut cw = CountWrite::from(h);
+
+        self.nar_renderer.write_nar(&mut cw, root_node)?;
+
+        Ok((cw.count(), cw.into_inner().finalize().into()))
+    }
+}
diff --git a/tvix/store/src/nar/renderer.rs b/tvix/store/src/nar/renderer.rs
new file mode 100644
index 000000000000..c10f2ddf52fa
--- /dev/null
+++ b/tvix/store/src/nar/renderer.rs
@@ -0,0 +1,136 @@
+use super::RenderError;
+use crate::{
+    blobservice::BlobService,
+    directoryservice::DirectoryService,
+    proto::{self, NamedNode},
+    B3Digest,
+};
+use nix_compat::nar;
+use std::io::{self, BufReader};
+use tracing::warn;
+
+/// A NAR renderer, using a blob_service, chunk_service and directory_service
+/// to render a NAR to a writer.
+#[derive(Clone)]
+pub struct NARRenderer<BS: BlobService, DS: DirectoryService> {
+    blob_service: BS,
+    directory_service: DS,
+}
+
+impl<BS: BlobService, DS: DirectoryService> NARRenderer<BS, DS> {
+    pub fn new(blob_service: BS, directory_service: DS) -> Self {
+        Self {
+            blob_service,
+            directory_service,
+        }
+    }
+
+    /// Consumes a [proto::node::Node] pointing to the root of a (store) path,
+    /// and writes the contents in NAR serialization to the passed
+    /// [std::io::Write].
+    ///
+    /// It uses the different clients in the struct to perform the necessary
+    /// lookups as it traverses the structure.
+    pub fn write_nar<W: std::io::Write>(
+        &self,
+        w: &mut W,
+        proto_root_node: &proto::node::Node,
+    ) -> Result<(), RenderError> {
+        // Initialize NAR writer
+        let nar_root_node = nar::writer::open(w).map_err(RenderError::NARWriterError)?;
+
+        self.walk_node(nar_root_node, proto_root_node)
+    }
+
+    /// Process an intermediate node in the structure.
+    /// This consumes the node.
+    fn walk_node(
+        &self,
+        nar_node: nar::writer::Node,
+        proto_node: &proto::node::Node,
+    ) -> Result<(), RenderError> {
+        match proto_node {
+            proto::node::Node::Symlink(proto_symlink_node) => {
+                nar_node
+                    .symlink(&proto_symlink_node.target)
+                    .map_err(RenderError::NARWriterError)?;
+            }
+            proto::node::Node::File(proto_file_node) => {
+                let digest = B3Digest::from_vec(proto_file_node.digest.clone()).map_err(|_e| {
+                    warn!(
+                        file_node = ?proto_file_node,
+                        "invalid digest length in file node",
+                    );
+
+                    RenderError::StoreError(crate::Error::StorageError(
+                        "invalid digest len in file node".to_string(),
+                    ))
+                })?;
+
+                let mut blob_reader = match self
+                    .blob_service
+                    .open_read(&digest)
+                    .map_err(RenderError::StoreError)?
+                {
+                    Some(blob_reader) => Ok(BufReader::new(blob_reader)),
+                    None => Err(RenderError::NARWriterError(io::Error::new(
+                        io::ErrorKind::NotFound,
+                        format!("blob with digest {} not found", &digest),
+                    ))),
+                }?;
+
+                nar_node
+                    .file(
+                        proto_file_node.executable,
+                        proto_file_node.size.into(),
+                        &mut blob_reader,
+                    )
+                    .map_err(RenderError::NARWriterError)?;
+            }
+            proto::node::Node::Directory(proto_directory_node) => {
+                let digest =
+                    B3Digest::from_vec(proto_directory_node.digest.to_vec()).map_err(|_e| {
+                        RenderError::StoreError(crate::Error::StorageError(
+                            "invalid digest len in directory node".to_string(),
+                        ))
+                    })?;
+
+                // look it up with the directory service
+                let resp = self
+                    .directory_service
+                    .get(&digest)
+                    .map_err(RenderError::StoreError)?;
+
+                match resp {
+                    // if it's None, that's an error!
+                    None => {
+                        return Err(RenderError::DirectoryNotFound(
+                            digest,
+                            proto_directory_node.name.to_owned(),
+                        ))
+                    }
+                    Some(proto_directory) => {
+                        // start a directory node
+                        let mut nar_node_directory =
+                            nar_node.directory().map_err(RenderError::NARWriterError)?;
+
+                        // for each node in the directory, create a new entry with its name,
+                        // and then invoke walk_node on that entry.
+                        for proto_node in proto_directory.nodes() {
+                            let child_node = nar_node_directory
+                                .entry(proto_node.get_name())
+                                .map_err(RenderError::NARWriterError)?;
+                            self.walk_node(child_node, &proto_node)?;
+                        }
+
+                        // close the directory
+                        nar_node_directory
+                            .close()
+                            .map_err(RenderError::NARWriterError)?;
+                    }
+                }
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/tvix/store/src/pathinfoservice/grpc.rs b/tvix/store/src/pathinfoservice/grpc.rs
new file mode 100644
index 000000000000..6bb774c668a3
--- /dev/null
+++ b/tvix/store/src/pathinfoservice/grpc.rs
@@ -0,0 +1,81 @@
+use super::PathInfoService;
+use crate::proto;
+use tonic::{transport::Channel, Code, Status};
+
+/// Connects to a (remote) tvix-store PathInfoService over gRPC.
+#[derive(Clone)]
+pub struct GRPCPathInfoService {
+    /// A handle into the active tokio runtime. Necessary to spawn tasks.
+    tokio_handle: tokio::runtime::Handle,
+
+    /// The internal reference to a gRPC client.
+    /// Cloning it is cheap, and it internally handles concurrent requests.
+    grpc_client: proto::path_info_service_client::PathInfoServiceClient<Channel>,
+}
+
+impl GRPCPathInfoService {
+    /// Construct a new [GRPCPathInfoService], by passing a handle to the tokio
+    /// runtime, and a gRPC client.
+    pub fn new(
+        tokio_handle: tokio::runtime::Handle,
+        grpc_client: proto::path_info_service_client::PathInfoServiceClient<Channel>,
+    ) -> Self {
+        Self {
+            tokio_handle,
+            grpc_client,
+        }
+    }
+
+    /// construct a [GRPCDirectoryService] from a [proto::path_info_service_client::PathInfoServiceClient<Channel>].
+    /// panics if called outside the context of a tokio runtime.
+    pub fn from_client(
+        grpc_client: proto::path_info_service_client::PathInfoServiceClient<Channel>,
+    ) -> Self {
+        Self {
+            tokio_handle: tokio::runtime::Handle::current(),
+            grpc_client,
+        }
+    }
+}
+
+impl PathInfoService for GRPCPathInfoService {
+    fn get(&self, digest: [u8; 20]) -> Result<Option<proto::PathInfo>, crate::Error> {
+        // Get a new handle to the gRPC client.
+        let mut grpc_client = self.grpc_client.clone();
+
+        let task: tokio::task::JoinHandle<Result<proto::PathInfo, Status>> =
+            self.tokio_handle.spawn(async move {
+                let path_info = grpc_client
+                    .get(proto::GetPathInfoRequest {
+                        by_what: Some(proto::get_path_info_request::ByWhat::ByOutputHash(
+                            digest.to_vec(),
+                        )),
+                    })
+                    .await?
+                    .into_inner();
+
+                Ok(path_info)
+            });
+
+        match self.tokio_handle.block_on(task)? {
+            Ok(path_info) => Ok(Some(path_info)),
+            Err(e) if e.code() == Code::NotFound => Ok(None),
+            Err(e) => Err(crate::Error::StorageError(e.to_string())),
+        }
+    }
+
+    fn put(&self, path_info: proto::PathInfo) -> Result<proto::PathInfo, crate::Error> {
+        // Get a new handle to the gRPC client.
+        let mut grpc_client = self.grpc_client.clone();
+
+        let task: tokio::task::JoinHandle<Result<proto::PathInfo, Status>> =
+            self.tokio_handle.spawn(async move {
+                let path_info = grpc_client.put(path_info).await?.into_inner();
+                Ok(path_info)
+            });
+
+        self.tokio_handle
+            .block_on(task)?
+            .map_err(|e| crate::Error::StorageError(e.to_string()))
+    }
+}
diff --git a/tvix/store/src/pathinfoservice/memory.rs b/tvix/store/src/pathinfoservice/memory.rs
new file mode 100644
index 000000000000..d0ff1976efab
--- /dev/null
+++ b/tvix/store/src/pathinfoservice/memory.rs
@@ -0,0 +1,41 @@
+use super::PathInfoService;
+use crate::{proto, Error};
+use std::{
+    collections::HashMap,
+    sync::{Arc, RwLock},
+};
+
+#[derive(Default)]
+pub struct MemoryPathInfoService {
+    db: Arc<RwLock<HashMap<[u8; 20], proto::PathInfo>>>,
+}
+
+impl PathInfoService for MemoryPathInfoService {
+    fn get(&self, digest: [u8; 20]) -> Result<Option<proto::PathInfo>, Error> {
+        let db = self.db.read().unwrap();
+
+        match db.get(&digest) {
+            None => Ok(None),
+            Some(path_info) => Ok(Some(path_info.clone())),
+        }
+    }
+
+    fn put(&self, path_info: proto::PathInfo) -> Result<proto::PathInfo, Error> {
+        // Call validate on the received PathInfo message.
+        match path_info.validate() {
+            Err(e) => Err(Error::InvalidRequest(format!(
+                "failed to validate PathInfo: {}",
+                e
+            ))),
+
+            // In case the PathInfo is valid, and we were able to extract a NixPath, store it in the database.
+            // This overwrites existing PathInfo objects.
+            Ok(nix_path) => {
+                let mut db = self.db.write().unwrap();
+                db.insert(nix_path.digest, path_info.clone());
+
+                Ok(path_info)
+            }
+        }
+    }
+}
diff --git a/tvix/store/src/pathinfoservice/mod.rs b/tvix/store/src/pathinfoservice/mod.rs
new file mode 100644
index 000000000000..6a34e09af478
--- /dev/null
+++ b/tvix/store/src/pathinfoservice/mod.rs
@@ -0,0 +1,20 @@
+mod grpc;
+mod memory;
+mod sled;
+
+use crate::{proto, Error};
+
+pub use self::grpc::GRPCPathInfoService;
+pub use self::memory::MemoryPathInfoService;
+pub use self::sled::SledPathInfoService;
+
+/// The base trait all PathInfo services need to implement.
+/// This is a simple get and put of [proto::Directory], returning their digest.
+pub trait PathInfoService {
+    /// Retrieve a PathInfo message by the output digest.
+    fn get(&self, digest: [u8; 20]) -> Result<Option<proto::PathInfo>, Error>;
+
+    /// Store a PathInfo message. Implementations MUST call validate and reject
+    /// invalid messages.
+    fn put(&self, path_info: proto::PathInfo) -> Result<proto::PathInfo, Error>;
+}
diff --git a/tvix/store/src/pathinfoservice/sled.rs b/tvix/store/src/pathinfoservice/sled.rs
new file mode 100644
index 000000000000..8776ebcbc106
--- /dev/null
+++ b/tvix/store/src/pathinfoservice/sled.rs
@@ -0,0 +1,76 @@
+use super::PathInfoService;
+use crate::{proto, Error};
+use prost::Message;
+use std::path::PathBuf;
+use tracing::warn;
+
+/// SledPathInfoService stores PathInfo in a [sled](https://github.com/spacejam/sled).
+///
+/// The PathInfo messages are stored as encoded protos, and keyed by their output hash,
+/// as that's currently the only request type available.
+#[derive(Clone)]
+pub struct SledPathInfoService {
+    db: sled::Db,
+}
+
+impl SledPathInfoService {
+    pub fn new(p: PathBuf) -> Result<Self, sled::Error> {
+        let config = sled::Config::default().use_compression(true).path(p);
+        let db = config.open()?;
+
+        Ok(Self { db })
+    }
+
+    pub fn new_temporary() -> Result<Self, sled::Error> {
+        let config = sled::Config::default().temporary(true);
+        let db = config.open()?;
+
+        Ok(Self { db })
+    }
+}
+
+impl PathInfoService for SledPathInfoService {
+    fn get(&self, digest: [u8; 20]) -> Result<Option<proto::PathInfo>, Error> {
+        match self.db.get(digest) {
+            Ok(None) => Ok(None),
+            Ok(Some(data)) => match proto::PathInfo::decode(&*data) {
+                Ok(path_info) => Ok(Some(path_info)),
+                Err(e) => {
+                    warn!("failed to decode stored PathInfo: {}", e);
+                    Err(Error::StorageError(format!(
+                        "failed to decode stored PathInfo: {}",
+                        e
+                    )))
+                }
+            },
+            Err(e) => {
+                warn!("failed to retrieve PathInfo: {}", e);
+                Err(Error::StorageError(format!(
+                    "failed to retrieve PathInfo: {}",
+                    e
+                )))
+            }
+        }
+    }
+
+    fn put(&self, path_info: proto::PathInfo) -> Result<proto::PathInfo, Error> {
+        // Call validate on the received PathInfo message.
+        match path_info.validate() {
+            Err(e) => Err(Error::InvalidRequest(format!(
+                "failed to validate PathInfo: {}",
+                e
+            ))),
+            // In case the PathInfo is valid, and we were able to extract a NixPath, store it in the database.
+            // This overwrites existing PathInfo objects.
+            Ok(nix_path) => match self.db.insert(nix_path.digest, path_info.encode_to_vec()) {
+                Ok(_) => Ok(path_info),
+                Err(e) => {
+                    warn!("failed to insert PathInfo: {}", e);
+                    Err(Error::StorageError(format! {
+                        "failed to insert PathInfo: {}", e
+                    }))
+                }
+            },
+        }
+    }
+}
diff --git a/tvix/store/src/proto/grpc_blobservice_wrapper.rs b/tvix/store/src/proto/grpc_blobservice_wrapper.rs
new file mode 100644
index 000000000000..3ec1d68872c7
--- /dev/null
+++ b/tvix/store/src/proto/grpc_blobservice_wrapper.rs
@@ -0,0 +1,130 @@
+use crate::{
+    blobservice::{BlobService, BlobWriter},
+    proto::sync_read_into_async_read::SyncReadIntoAsyncRead,
+    B3Digest,
+};
+use std::{collections::VecDeque, io, pin::Pin};
+use tokio::task;
+use tokio_stream::StreamExt;
+use tokio_util::io::ReaderStream;
+use tonic::{async_trait, Request, Response, Status, Streaming};
+use tracing::{instrument, warn};
+
+pub struct GRPCBlobServiceWrapper<BS: BlobService> {
+    blob_service: BS,
+}
+
+impl<BS: BlobService> From<BS> for GRPCBlobServiceWrapper<BS> {
+    fn from(value: BS) -> Self {
+        Self {
+            blob_service: value,
+        }
+    }
+}
+
+#[async_trait]
+impl<BS: BlobService + Send + Sync + Clone + 'static> super::blob_service_server::BlobService
+    for GRPCBlobServiceWrapper<BS>
+{
+    // https://github.com/tokio-rs/tokio/issues/2723#issuecomment-1534723933
+    type ReadStream =
+        Pin<Box<dyn futures::Stream<Item = Result<super::BlobChunk, Status>> + Send + 'static>>;
+
+    #[instrument(skip(self))]
+    async fn stat(
+        &self,
+        request: Request<super::StatBlobRequest>,
+    ) -> Result<Response<super::BlobMeta>, Status> {
+        let rq = request.into_inner();
+        let req_digest = B3Digest::from_vec(rq.digest)
+            .map_err(|_e| Status::invalid_argument("invalid digest length"))?;
+
+        if rq.include_chunks || rq.include_bao {
+            return Err(Status::internal("not implemented"));
+        }
+
+        match self.blob_service.has(&req_digest) {
+            Ok(true) => Ok(Response::new(super::BlobMeta::default())),
+            Ok(false) => Err(Status::not_found(format!("blob {} not found", &req_digest))),
+            Err(e) => Err(e.into()),
+        }
+    }
+
+    #[instrument(skip(self))]
+    async fn read(
+        &self,
+        request: Request<super::ReadBlobRequest>,
+    ) -> Result<Response<Self::ReadStream>, Status> {
+        let rq = request.into_inner();
+
+        let req_digest = B3Digest::from_vec(rq.digest)
+            .map_err(|_e| Status::invalid_argument("invalid digest length"))?;
+
+        match self.blob_service.open_read(&req_digest) {
+            Ok(Some(reader)) => {
+                let async_reader: SyncReadIntoAsyncRead<_, bytes::BytesMut> = reader.into();
+
+                fn stream_mapper(
+                    x: Result<bytes::Bytes, io::Error>,
+                ) -> Result<super::BlobChunk, Status> {
+                    match x {
+                        Ok(bytes) => Ok(super::BlobChunk {
+                            data: bytes.to_vec(),
+                        }),
+                        Err(e) => Err(Status::from(e)),
+                    }
+                }
+
+                let chunks_stream = ReaderStream::new(async_reader).map(stream_mapper);
+                Ok(Response::new(Box::pin(chunks_stream)))
+            }
+            Ok(None) => Err(Status::not_found(format!("blob {} not found", &req_digest))),
+            Err(e) => Err(e.into()),
+        }
+    }
+
+    #[instrument(skip(self))]
+    async fn put(
+        &self,
+        request: Request<Streaming<super::BlobChunk>>,
+    ) -> Result<Response<super::PutBlobResponse>, Status> {
+        let req_inner = request.into_inner();
+
+        let data_stream = req_inner.map(|x| {
+            x.map(|x| VecDeque::from(x.data))
+                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
+        });
+
+        let data_reader = tokio_util::io::StreamReader::new(data_stream);
+
+        // prepare a writer, which we'll use in the blocking task below.
+        let mut writer = self
+            .blob_service
+            .open_write()
+            .map_err(|e| Status::internal(format!("unable to open for write: {}", e)))?;
+
+        let result = task::spawn_blocking(move || -> Result<super::PutBlobResponse, Status> {
+            // construct a sync reader to the data
+            let mut reader = tokio_util::io::SyncIoBridge::new(data_reader);
+
+            io::copy(&mut reader, &mut writer).map_err(|e| {
+                warn!("error copying: {}", e);
+                Status::internal("error copying")
+            })?;
+
+            let digest = writer
+                .close()
+                .map_err(|e| {
+                    warn!("error closing stream: {}", e);
+                    Status::internal("error closing stream")
+                })?
+                .to_vec();
+
+            Ok(super::PutBlobResponse { digest })
+        })
+        .await
+        .map_err(|_| Status::internal("failed to wait for task"))??;
+
+        Ok(Response::new(result))
+    }
+}
diff --git a/tvix/store/src/proto/grpc_directoryservice_wrapper.rs b/tvix/store/src/proto/grpc_directoryservice_wrapper.rs
new file mode 100644
index 000000000000..6d2df310137f
--- /dev/null
+++ b/tvix/store/src/proto/grpc_directoryservice_wrapper.rs
@@ -0,0 +1,177 @@
+use crate::proto;
+use crate::{directoryservice::DirectoryService, B3Digest};
+use std::collections::HashMap;
+use tokio::{sync::mpsc::channel, task};
+use tokio_stream::wrappers::ReceiverStream;
+use tonic::{async_trait, Request, Response, Status, Streaming};
+use tracing::{debug, instrument, warn};
+
+pub struct GRPCDirectoryServiceWrapper<C: DirectoryService> {
+    directory_service: C,
+}
+
+impl<DS: DirectoryService> From<DS> for GRPCDirectoryServiceWrapper<DS> {
+    fn from(value: DS) -> Self {
+        Self {
+            directory_service: value,
+        }
+    }
+}
+
+#[async_trait]
+impl<DS: DirectoryService + Send + Sync + Clone + 'static>
+    proto::directory_service_server::DirectoryService for GRPCDirectoryServiceWrapper<DS>
+{
+    type GetStream = ReceiverStream<tonic::Result<proto::Directory, Status>>;
+
+    #[instrument(skip(self))]
+    async fn get(
+        &self,
+        request: Request<proto::GetDirectoryRequest>,
+    ) -> Result<Response<Self::GetStream>, Status> {
+        let (tx, rx) = channel(5);
+
+        let req_inner = request.into_inner();
+
+        let directory_service = self.directory_service.clone();
+
+        let _task = {
+            // look at the digest in the request and put it in the top of the queue.
+            match &req_inner.by_what {
+                None => return Err(Status::invalid_argument("by_what needs to be specified")),
+                Some(proto::get_directory_request::ByWhat::Digest(digest)) => {
+                    let digest = B3Digest::from_vec(digest.to_vec())
+                        .map_err(|_e| Status::invalid_argument("invalid digest length"))?;
+
+                    task::spawn(async move {
+                        if !req_inner.recursive {
+                            let e: Result<proto::Directory, Status> =
+                                match directory_service.get(&digest) {
+                                    Ok(Some(directory)) => Ok(directory),
+                                    Ok(None) => Err(Status::not_found(format!(
+                                        "directory {} not found",
+                                        digest
+                                    ))),
+                                    Err(e) => Err(e.into()),
+                                };
+
+                            if tx.send(e).await.is_err() {
+                                debug!("receiver dropped");
+                            }
+                        } else {
+                            // If recursive was requested, traverse via get_recursive.
+                            let directories_it = directory_service.get_recursive(&digest);
+
+                            for e in directories_it {
+                                // map err in res from Error to Status
+                                let res = e.map_err(|e| Status::internal(e.to_string()));
+                                if tx.send(res).await.is_err() {
+                                    debug!("receiver dropped");
+                                    break;
+                                }
+                            }
+                        }
+                    });
+                }
+            }
+        };
+
+        let receiver_stream = ReceiverStream::new(rx);
+        Ok(Response::new(receiver_stream))
+    }
+
+    #[instrument(skip(self, request))]
+    async fn put(
+        &self,
+        request: Request<Streaming<proto::Directory>>,
+    ) -> Result<Response<proto::PutDirectoryResponse>, Status> {
+        let mut req_inner = request.into_inner();
+        // TODO: let this use DirectoryPutter to the store it's connected to,
+        // and move the validation logic into [SimplePutter].
+
+        // This keeps track of the seen directory keys, and their size.
+        // This is used to validate the size field of a reference to a previously sent directory.
+        // We don't need to keep the contents around, they're stored in the DB.
+        let mut seen_directories_sizes: HashMap<B3Digest, u32> = HashMap::new();
+        let mut last_directory_dgst: Option<B3Digest> = None;
+
+        // Consume directories, and insert them into the store.
+        // Reject directory messages that refer to Directories not sent in the same stream.
+        while let Some(directory) = req_inner.message().await? {
+            // validate the directory itself.
+            if let Err(e) = directory.validate() {
+                return Err(Status::invalid_argument(format!(
+                    "directory {} failed validation: {}",
+                    directory.digest(),
+                    e,
+                )));
+            }
+
+            // for each child directory this directory refers to, we need
+            // to ensure it has been seen already in this stream, and that the size
+            // matches what we recorded.
+            for child_directory in &directory.directories {
+                let child_directory_digest = B3Digest::from_vec(child_directory.digest.to_vec())
+                    .map_err(|_e| Status::internal("invalid child directory digest len"))?;
+
+                match seen_directories_sizes.get(&child_directory_digest) {
+                    None => {
+                        return Err(Status::invalid_argument(format!(
+                            "child directory '{}' ({}) in directory '{}' not seen yet",
+                            child_directory.name,
+                            &child_directory_digest,
+                            &directory.digest(),
+                        )));
+                    }
+                    Some(seen_child_directory_size) => {
+                        if seen_child_directory_size != &child_directory.size {
+                            return Err(Status::invalid_argument(format!(
+                                    "child directory '{}' ({}) in directory '{}' referred with wrong size, expected {}, actual {}",
+                                    child_directory.name,
+                                    &child_directory_digest,
+                                    &directory.digest(),
+                                    seen_child_directory_size,
+                                    child_directory.size,
+                            )));
+                        }
+                    }
+                }
+            }
+
+            // NOTE: We can't know if a directory we're receiving actually is
+            // part of the closure, because we receive directories from the leaf nodes up to
+            // the root.
+            // The only thing we could to would be doing a final check when the
+            // last Directory was received, that all Directories received so far are
+            // reachable from that (root) node.
+
+            let dgst = directory.digest();
+            seen_directories_sizes.insert(dgst.clone(), directory.size());
+            last_directory_dgst = Some(dgst.clone());
+
+            // check if the directory already exists in the database. We can skip
+            // inserting if it's already there, as that'd be a no-op.
+            match self.directory_service.get(&dgst) {
+                Err(e) => {
+                    warn!("error checking if directory already exists: {}", e);
+                    return Err(e.into());
+                }
+                // skip if already exists
+                Ok(Some(_)) => {}
+                // insert if it doesn't already exist
+                Ok(None) => {
+                    self.directory_service.put(directory)?;
+                }
+            }
+        }
+
+        // We're done receiving. peek at last_directory_digest and either return the digest,
+        // or an error, if we received an empty stream.
+        match last_directory_dgst {
+            None => Err(Status::invalid_argument("no directories received")),
+            Some(last_directory_dgst) => Ok(Response::new(proto::PutDirectoryResponse {
+                root_digest: last_directory_dgst.to_vec(),
+            })),
+        }
+    }
+}
diff --git a/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs b/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
new file mode 100644
index 000000000000..e82557b3a06c
--- /dev/null
+++ b/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
@@ -0,0 +1,93 @@
+use crate::nar::RenderError;
+use crate::proto;
+use crate::{nar::NARCalculationService, pathinfoservice::PathInfoService};
+use tonic::{async_trait, Request, Response, Result, Status};
+use tracing::{instrument, warn};
+
+pub struct GRPCPathInfoServiceWrapper<PS: PathInfoService, NS: NARCalculationService> {
+    path_info_service: PS,
+    nar_calculation_service: NS,
+}
+
+impl<PS: PathInfoService, NS: NARCalculationService> GRPCPathInfoServiceWrapper<PS, NS> {
+    pub fn new(path_info_service: PS, nar_calculation_service: NS) -> Self {
+        Self {
+            path_info_service,
+            nar_calculation_service,
+        }
+    }
+}
+
+#[async_trait]
+impl<
+        PS: PathInfoService + Send + Sync + 'static,
+        NS: NARCalculationService + Send + Sync + 'static,
+    > proto::path_info_service_server::PathInfoService for GRPCPathInfoServiceWrapper<PS, NS>
+{
+    #[instrument(skip(self))]
+    async fn get(
+        &self,
+        request: Request<proto::GetPathInfoRequest>,
+    ) -> Result<Response<proto::PathInfo>> {
+        match request.into_inner().by_what {
+            None => Err(Status::unimplemented("by_what needs to be specified")),
+            Some(proto::get_path_info_request::ByWhat::ByOutputHash(digest)) => {
+                let digest: [u8; 20] = digest
+                    .try_into()
+                    .map_err(|_e| Status::invalid_argument("invalid digest length"))?;
+                match self.path_info_service.get(digest) {
+                    Ok(None) => Err(Status::not_found("PathInfo not found")),
+                    Ok(Some(path_info)) => Ok(Response::new(path_info)),
+                    Err(e) => {
+                        warn!("failed to retrieve PathInfo: {}", e);
+                        Err(e.into())
+                    }
+                }
+            }
+        }
+    }
+
+    #[instrument(skip(self))]
+    async fn put(&self, request: Request<proto::PathInfo>) -> Result<Response<proto::PathInfo>> {
+        let path_info = request.into_inner();
+
+        // Store the PathInfo in the client. Clients MUST validate the data
+        // they receive, so we don't validate additionally here.
+        match self.path_info_service.put(path_info) {
+            Ok(path_info_new) => Ok(Response::new(path_info_new)),
+            Err(e) => {
+                warn!("failed to insert PathInfo: {}", e);
+                Err(e.into())
+            }
+        }
+    }
+
+    #[instrument(skip(self))]
+    async fn calculate_nar(
+        &self,
+        request: Request<proto::Node>,
+    ) -> Result<Response<proto::CalculateNarResponse>> {
+        match request.into_inner().node {
+            None => Err(Status::invalid_argument("no root node sent")),
+            Some(root_node) => match self.nar_calculation_service.calculate_nar(&root_node) {
+                Ok((nar_size, nar_sha256)) => Ok(Response::new(proto::CalculateNarResponse {
+                    nar_size,
+                    nar_sha256: nar_sha256.to_vec(),
+                })),
+                Err(e) => Err(e.into()),
+            },
+        }
+    }
+}
+
+impl From<RenderError> for tonic::Status {
+    fn from(value: RenderError) -> Self {
+        match value {
+            RenderError::BlobNotFound(_, _) => Self::not_found(value.to_string()),
+            RenderError::DirectoryNotFound(_, _) => Self::not_found(value.to_string()),
+            RenderError::NARWriterError(_) => Self::internal(value.to_string()),
+            RenderError::StoreError(_) => Self::internal(value.to_string()),
+            RenderError::UnexpectedBlobMeta(_, _, _, _) => Self::internal(value.to_string()),
+        }
+    }
+}
diff --git a/tvix/store/src/proto/mod.rs b/tvix/store/src/proto/mod.rs
new file mode 100644
index 000000000000..4db0b9731edc
--- /dev/null
+++ b/tvix/store/src/proto/mod.rs
@@ -0,0 +1,370 @@
+#![allow(clippy::derive_partial_eq_without_eq)]
+// https://github.com/hyperium/tonic/issues/1056
+use std::{collections::HashSet, iter::Peekable};
+use thiserror::Error;
+
+use prost::Message;
+
+use nix_compat::store_path::{self, StorePath};
+
+mod grpc_blobservice_wrapper;
+mod grpc_directoryservice_wrapper;
+mod grpc_pathinfoservice_wrapper;
+
+mod sync_read_into_async_read;
+
+pub use grpc_blobservice_wrapper::GRPCBlobServiceWrapper;
+pub use grpc_directoryservice_wrapper::GRPCDirectoryServiceWrapper;
+pub use grpc_pathinfoservice_wrapper::GRPCPathInfoServiceWrapper;
+
+use crate::B3Digest;
+
+tonic::include_proto!("tvix.store.v1");
+
+#[cfg(feature = "reflection")]
+/// Compiled file descriptors for implementing [gRPC
+/// reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) with e.g.
+/// [`tonic_reflection`](https://docs.rs/tonic-reflection).
+pub const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("tvix.store.v1");
+
+#[cfg(test)]
+mod tests;
+
+/// Errors that can occur during the validation of Directory messages.
+#[derive(Debug, PartialEq, Eq, Error)]
+pub enum ValidateDirectoryError {
+    /// Elements are not in sorted order
+    #[error("{0} is not sorted")]
+    WrongSorting(String),
+    /// Multiple elements with the same name encountered
+    #[error("{0} is a duplicate name")]
+    DuplicateName(String),
+    /// Invalid name encountered
+    #[error("Invalid name in {0}")]
+    InvalidName(String),
+    /// Invalid digest length encountered
+    #[error("Invalid Digest length: {0}")]
+    InvalidDigestLen(usize),
+}
+
+/// Errors that can occur during the validation of PathInfo messages.
+#[derive(Debug, Error, PartialEq)]
+pub enum ValidatePathInfoError {
+    /// No node present
+    #[error("No node present")]
+    NoNodePresent(),
+
+    /// Invalid node name encountered.
+    #[error("Failed to parse {0} as StorePath: {1}")]
+    InvalidNodeName(String, store_path::Error),
+
+    /// The digest the (root) node refers to has invalid length.
+    #[error("Invalid Digest length: {0}")]
+    InvalidDigestLen(usize),
+
+    /// The number of references in the narinfo.reference_names field does not match
+    /// the number of references in the .references field.
+    #[error("Inconsistent Number of References: {0} (references) vs {0} (narinfo)")]
+    InconsistentNumberOfReferences(usize, usize),
+}
+
+/// Checks a Node name for validity as an intermediate node, and returns an
+/// error that's generated from the supplied constructor.
+///
+/// We disallow slashes, null bytes, '.', '..' and the empty string.
+fn validate_node_name<E>(name: &str, err: fn(String) -> E) -> Result<(), E> {
+    if name.is_empty() || name == ".." || name == "." || name.contains('\x00') || name.contains('/')
+    {
+        return Err(err(name.to_string()));
+    }
+    Ok(())
+}
+
+/// Checks a digest for validity.
+/// Digests are 32 bytes long, as we store blake3 digests.
+fn validate_digest<E>(digest: &Vec<u8>, err: fn(usize) -> E) -> Result<(), E> {
+    if digest.len() != 32 {
+        return Err(err(digest.len()));
+    }
+    Ok(())
+}
+
+/// Parses a root node name.
+///
+/// On success, this returns the parsed [StorePath].
+/// On error, it returns an error generated from the supplied constructor.
+fn parse_node_name_root<E>(
+    name: &str,
+    err: fn(String, store_path::Error) -> E,
+) -> Result<StorePath, E> {
+    match StorePath::from_string(name) {
+        Ok(np) => Ok(np),
+        Err(e) => Err(err(name.to_string(), e)),
+    }
+}
+
+impl PathInfo {
+    /// validate performs some checks on the PathInfo struct,
+    /// Returning either a [StorePath] of the root node, or a
+    /// [ValidatePathInfoError].
+    pub fn validate(&self) -> Result<StorePath, ValidatePathInfoError> {
+        // If there is a narinfo field populated, ensure the number of references there
+        // matches PathInfo.references count.
+        if let Some(narinfo) = &self.narinfo {
+            if narinfo.reference_names.len() != self.references.len() {
+                return Err(ValidatePathInfoError::InconsistentNumberOfReferences(
+                    narinfo.reference_names.len(),
+                    self.references.len(),
+                ));
+            }
+        }
+        // FUTUREWORK: parse references in reference_names. ensure they start
+        // with storeDir, and use the same digest as in self.references.
+
+        // Ensure there is a (root) node present, and it properly parses to a [StorePath].
+        let root_nix_path = match &self.node {
+            None => {
+                return Err(ValidatePathInfoError::NoNodePresent());
+            }
+            Some(Node { node }) => match node {
+                None => {
+                    return Err(ValidatePathInfoError::NoNodePresent());
+                }
+                Some(node::Node::Directory(directory_node)) => {
+                    // ensure the digest has the appropriate size.
+                    validate_digest(
+                        &directory_node.digest,
+                        ValidatePathInfoError::InvalidDigestLen,
+                    )?;
+
+                    // parse the name
+                    parse_node_name_root(
+                        &directory_node.name,
+                        ValidatePathInfoError::InvalidNodeName,
+                    )?
+                }
+                Some(node::Node::File(file_node)) => {
+                    // ensure the digest has the appropriate size.
+                    validate_digest(&file_node.digest, ValidatePathInfoError::InvalidDigestLen)?;
+
+                    // parse the name
+                    parse_node_name_root(&file_node.name, ValidatePathInfoError::InvalidNodeName)?
+                }
+                Some(node::Node::Symlink(symlink_node)) => {
+                    // parse the name
+                    parse_node_name_root(
+                        &symlink_node.name,
+                        ValidatePathInfoError::InvalidNodeName,
+                    )?
+                }
+            },
+        };
+
+        // return the root nix path
+        Ok(root_nix_path)
+    }
+}
+
+/// NamedNode is implemented for [FileNode], [DirectoryNode] and [SymlinkNode]
+/// and [node::Node], so we can ask all of them for the name easily.
+pub trait NamedNode {
+    fn get_name(&self) -> &str;
+}
+
+impl NamedNode for &FileNode {
+    fn get_name(&self) -> &str {
+        self.name.as_str()
+    }
+}
+
+impl NamedNode for &DirectoryNode {
+    fn get_name(&self) -> &str {
+        self.name.as_str()
+    }
+}
+
+impl NamedNode for &SymlinkNode {
+    fn get_name(&self) -> &str {
+        self.name.as_str()
+    }
+}
+
+impl NamedNode for node::Node {
+    fn get_name(&self) -> &str {
+        match self {
+            node::Node::File(node_file) => &node_file.name,
+            node::Node::Directory(node_directory) => &node_directory.name,
+            node::Node::Symlink(node_symlink) => &node_symlink.name,
+        }
+    }
+}
+
+/// Accepts a name, and a mutable reference to the previous name.
+/// If the passed name is larger than the previous one, the reference is updated.
+/// If it's not, an error is returned.
+fn update_if_lt_prev<'n>(
+    prev_name: &mut &'n str,
+    name: &'n str,
+) -> Result<(), ValidateDirectoryError> {
+    if *name < **prev_name {
+        return Err(ValidateDirectoryError::WrongSorting(name.to_string()));
+    }
+    *prev_name = name;
+    Ok(())
+}
+
+/// Inserts the given name into a HashSet if it's not already in there.
+/// If it is, an error is returned.
+fn insert_once<'n>(
+    seen_names: &mut HashSet<&'n str>,
+    name: &'n str,
+) -> Result<(), ValidateDirectoryError> {
+    if seen_names.get(name).is_some() {
+        return Err(ValidateDirectoryError::DuplicateName(name.to_string()));
+    }
+    seen_names.insert(name);
+    Ok(())
+}
+
+impl Directory {
+    /// The size of a directory is the number of all regular and symlink elements,
+    /// the number of directory elements, and their size fields.
+    pub fn size(&self) -> u32 {
+        self.files.len() as u32
+            + self.symlinks.len() as u32
+            + self
+                .directories
+                .iter()
+                .fold(0, |acc: u32, e| (acc + 1 + e.size))
+    }
+
+    /// Calculates the digest of a Directory, which is the blake3 hash of a
+    /// Directory protobuf message, serialized in protobuf canonical form.
+    pub fn digest(&self) -> B3Digest {
+        let mut hasher = blake3::Hasher::new();
+
+        let vec = hasher
+            .update(&self.encode_to_vec())
+            .finalize()
+            .as_bytes()
+            .to_vec();
+        B3Digest::from_vec(vec).unwrap()
+    }
+
+    /// validate checks the directory for invalid data, such as:
+    /// - violations of name restrictions
+    /// - invalid digest lengths
+    /// - not properly sorted lists
+    /// - duplicate names in the three lists
+    pub fn validate(&self) -> Result<(), ValidateDirectoryError> {
+        let mut seen_names: HashSet<&str> = HashSet::new();
+
+        let mut last_directory_name: &str = "";
+        let mut last_file_name: &str = "";
+        let mut last_symlink_name: &str = "";
+
+        // check directories
+        for directory_node in &self.directories {
+            validate_node_name(&directory_node.name, ValidateDirectoryError::InvalidName)?;
+            validate_digest(
+                &directory_node.digest,
+                ValidateDirectoryError::InvalidDigestLen,
+            )?;
+
+            update_if_lt_prev(&mut last_directory_name, directory_node.name.as_str())?;
+            insert_once(&mut seen_names, directory_node.name.as_str())?;
+        }
+
+        // check files
+        for file_node in &self.files {
+            validate_node_name(&file_node.name, ValidateDirectoryError::InvalidName)?;
+            validate_digest(&file_node.digest, ValidateDirectoryError::InvalidDigestLen)?;
+
+            update_if_lt_prev(&mut last_file_name, file_node.name.as_str())?;
+            insert_once(&mut seen_names, file_node.name.as_str())?;
+        }
+
+        // check symlinks
+        for symlink_node in &self.symlinks {
+            validate_node_name(&symlink_node.name, ValidateDirectoryError::InvalidName)?;
+
+            update_if_lt_prev(&mut last_symlink_name, symlink_node.name.as_str())?;
+            insert_once(&mut seen_names, symlink_node.name.as_str())?;
+        }
+
+        Ok(())
+    }
+
+    /// Allows iterating over all three nodes ([DirectoryNode], [FileNode],
+    /// [SymlinkNode]) in an ordered fashion, as long as the individual lists
+    /// are sorted (which can be checked by the [Directory::validate]).
+    pub fn nodes(&self) -> DirectoryNodesIterator {
+        return DirectoryNodesIterator {
+            i_directories: self.directories.iter().peekable(),
+            i_files: self.files.iter().peekable(),
+            i_symlinks: self.symlinks.iter().peekable(),
+        };
+    }
+}
+
+/// Struct to hold the state of an iterator over all nodes of a Directory.
+///
+/// Internally, this keeps peekable Iterators over all three lists of a
+/// Directory message.
+pub struct DirectoryNodesIterator<'a> {
+    // directory: &Directory,
+    i_directories: Peekable<std::slice::Iter<'a, DirectoryNode>>,
+    i_files: Peekable<std::slice::Iter<'a, FileNode>>,
+    i_symlinks: Peekable<std::slice::Iter<'a, SymlinkNode>>,
+}
+
+/// looks at two elements implementing NamedNode, and returns true if "left
+/// is smaller / comes first".
+///
+/// Some(_) is preferred over None.
+fn left_name_lt_right<A: NamedNode, B: NamedNode>(left: Option<&A>, right: Option<&B>) -> bool {
+    match left {
+        // if left is None, right always wins
+        None => false,
+        Some(left_inner) => {
+            // left is Some.
+            match right {
+                // left is Some, right is None - left wins.
+                None => true,
+                Some(right_inner) => {
+                    // both are Some - compare the name.
+                    return left_inner.get_name() < right_inner.get_name();
+                }
+            }
+        }
+    }
+}
+
+impl Iterator for DirectoryNodesIterator<'_> {
+    type Item = node::Node;
+
+    // next returns the next node in the Directory.
+    // we peek at all three internal iterators, and pick the one with the
+    // smallest name, to ensure lexicographical ordering.
+    // The individual lists are already known to be sorted.
+    fn next(&mut self) -> Option<Self::Item> {
+        if left_name_lt_right(self.i_directories.peek(), self.i_files.peek()) {
+            // i_directories is still in the game, compare with symlinks
+            if left_name_lt_right(self.i_directories.peek(), self.i_symlinks.peek()) {
+                self.i_directories
+                    .next()
+                    .cloned()
+                    .map(node::Node::Directory)
+            } else {
+                self.i_symlinks.next().cloned().map(node::Node::Symlink)
+            }
+        } else {
+            // i_files is still in the game, compare with symlinks
+            if left_name_lt_right(self.i_files.peek(), self.i_symlinks.peek()) {
+                self.i_files.next().cloned().map(node::Node::File)
+            } else {
+                self.i_symlinks.next().cloned().map(node::Node::Symlink)
+            }
+        }
+    }
+}
diff --git a/tvix/store/src/proto/sync_read_into_async_read.rs b/tvix/store/src/proto/sync_read_into_async_read.rs
new file mode 100644
index 000000000000..0a0ef019781c
--- /dev/null
+++ b/tvix/store/src/proto/sync_read_into_async_read.rs
@@ -0,0 +1,158 @@
+use bytes::Buf;
+use core::task::Poll::Ready;
+use futures::ready;
+use futures::Future;
+use std::io;
+use std::io::Read;
+use std::pin::Pin;
+use std::sync::Arc;
+use std::task::Context;
+use std::task::Poll;
+use tokio::io::AsyncRead;
+use tokio::runtime::Handle;
+use tokio::sync::Mutex;
+use tokio::task::JoinHandle;
+
+#[derive(Debug)]
+enum State<Buf: bytes::Buf + bytes::BufMut> {
+    Idle(Option<Buf>),
+    Busy(JoinHandle<(io::Result<usize>, Buf)>),
+}
+
+use State::{Busy, Idle};
+
+/// Use a [`SyncReadIntoAsyncRead`] to asynchronously read from a
+/// synchronous API.
+#[derive(Debug)]
+pub struct SyncReadIntoAsyncRead<R: Read + Send, Buf: bytes::Buf + bytes::BufMut> {
+    state: Mutex<State<Buf>>,
+    reader: Arc<Mutex<R>>,
+    rt: Handle,
+}
+
+impl<R: Read + Send, Buf: bytes::Buf + bytes::BufMut> SyncReadIntoAsyncRead<R, Buf> {
+    /// This must be called from within a Tokio runtime context, or else it will panic.
+    #[track_caller]
+    pub fn new(rt: Handle, reader: R) -> Self {
+        Self {
+            rt,
+            state: State::Idle(None).into(),
+            reader: Arc::new(reader.into()),
+        }
+    }
+
+    /// This must be called from within a Tokio runtime context, or else it will panic.
+    pub fn new_with_reader(readable: R) -> Self {
+        Self::new(Handle::current(), readable)
+    }
+}
+
+/// Repeats operations that are interrupted.
+macro_rules! uninterruptibly {
+    ($e:expr) => {{
+        loop {
+            match $e {
+                Err(ref e) if e.kind() == io::ErrorKind::Interrupted => {}
+                res => break res,
+            }
+        }
+    }};
+}
+
+impl<
+        R: Read + Send + 'static + std::marker::Unpin,
+        Buf: bytes::Buf + bytes::BufMut + Send + Default + std::marker::Unpin + 'static,
+    > AsyncRead for SyncReadIntoAsyncRead<R, Buf>
+{
+    fn poll_read(
+        self: Pin<&mut Self>,
+        cx: &mut Context<'_>,
+        dst: &mut tokio::io::ReadBuf<'_>,
+    ) -> Poll<io::Result<()>> {
+        let me = self.get_mut();
+        // Do we need this mutex?
+        let state = me.state.get_mut();
+
+        loop {
+            match state {
+                Idle(ref mut buf_cell) => {
+                    let mut buf = buf_cell.take().unwrap_or_default();
+
+                    if buf.has_remaining() {
+                        // Here, we will split the `buf` into `[..dst.remaining()... ; rest ]`
+                        // The `rest` is stuffed into the `buf_cell` for further poll_read.
+                        // The other is completely consumed into the unfilled destination.
+                        // `rest` can be empty.
+                        let mut adjusted_src =
+                            buf.copy_to_bytes(std::cmp::min(buf.remaining(), dst.remaining()));
+                        let copied_size = adjusted_src.remaining();
+                        adjusted_src.copy_to_slice(dst.initialize_unfilled_to(copied_size));
+                        dst.set_filled(copied_size);
+                        *buf_cell = Some(buf);
+                        return Ready(Ok(()));
+                    }
+
+                    let reader = me.reader.clone();
+                    *state = Busy(me.rt.spawn_blocking(move || {
+                        let result = uninterruptibly!(reader.blocking_lock().read(
+                            // SAFETY: `reader.read` will *ONLY* write initialized bytes
+                            // and never *READ* uninitialized bytes
+                            // inside this buffer.
+                            //
+                            // Furthermore, casting the slice as `*mut [u8]`
+                            // is safe because it has the same layout.
+                            //
+                            // Finally, the pointer obtained is valid and owned
+                            // by `buf` only as we have a valid mutable reference
+                            // to it, it is valid for write.
+                            //
+                            // Here, we copy an nightly API: https://doc.rust-lang.org/stable/src/core/mem/maybe_uninit.rs.html#994-998
+                            unsafe {
+                                &mut *(buf.chunk_mut().as_uninit_slice_mut()
+                                    as *mut [std::mem::MaybeUninit<u8>]
+                                    as *mut [u8])
+                            }
+                        ));
+
+                        if let Ok(n) = result {
+                            // SAFETY: given we initialize `n` bytes, we can move `n` bytes
+                            // forward.
+                            unsafe {
+                                buf.advance_mut(n);
+                            }
+                        }
+
+                        (result, buf)
+                    }));
+                }
+                Busy(ref mut rx) => {
+                    let (result, mut buf) = ready!(Pin::new(rx).poll(cx))?;
+
+                    match result {
+                        Ok(n) => {
+                            if n > 0 {
+                                let remaining = std::cmp::min(n, dst.remaining());
+                                let mut adjusted_src = buf.copy_to_bytes(remaining);
+                                adjusted_src.copy_to_slice(dst.initialize_unfilled_to(remaining));
+                                dst.advance(remaining);
+                            }
+                            *state = Idle(Some(buf));
+                            return Ready(Ok(()));
+                        }
+                        Err(e) => {
+                            *state = Idle(None);
+                            return Ready(Err(e));
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+impl<R: Read + Send, Buf: bytes::Buf + bytes::BufMut> From<R> for SyncReadIntoAsyncRead<R, Buf> {
+    /// This must be called from within a Tokio runtime context, or else it will panic.
+    fn from(value: R) -> Self {
+        Self::new_with_reader(value)
+    }
+}
diff --git a/tvix/store/src/proto/tests/directory.rs b/tvix/store/src/proto/tests/directory.rs
new file mode 100644
index 000000000000..8d6ca7241d7a
--- /dev/null
+++ b/tvix/store/src/proto/tests/directory.rs
@@ -0,0 +1,289 @@
+use crate::{
+    proto::{Directory, DirectoryNode, FileNode, SymlinkNode, ValidateDirectoryError},
+    B3Digest,
+};
+use lazy_static::lazy_static;
+
+lazy_static! {
+    static ref DUMMY_DIGEST: [u8; 32] = [
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00,
+    ];
+}
+#[test]
+fn size() {
+    {
+        let d = Directory::default();
+        assert_eq!(d.size(), 0);
+    }
+    {
+        let d = Directory {
+            directories: vec![DirectoryNode {
+                name: String::from("foo"),
+                digest: DUMMY_DIGEST.to_vec(),
+                size: 0,
+            }],
+            ..Default::default()
+        };
+        assert_eq!(d.size(), 1);
+    }
+    {
+        let d = Directory {
+            directories: vec![DirectoryNode {
+                name: String::from("foo"),
+                digest: DUMMY_DIGEST.to_vec(),
+                size: 4,
+            }],
+            ..Default::default()
+        };
+        assert_eq!(d.size(), 5);
+    }
+    {
+        let d = Directory {
+            files: vec![FileNode {
+                name: String::from("foo"),
+                digest: DUMMY_DIGEST.to_vec(),
+                size: 42,
+                executable: false,
+            }],
+            ..Default::default()
+        };
+        assert_eq!(d.size(), 1);
+    }
+    {
+        let d = Directory {
+            symlinks: vec![SymlinkNode {
+                name: String::from("foo"),
+                target: String::from("bar"),
+            }],
+            ..Default::default()
+        };
+        assert_eq!(d.size(), 1);
+    }
+}
+
+#[test]
+fn digest() {
+    let d = Directory::default();
+
+    assert_eq!(
+        d.digest(),
+        B3Digest::from_vec(vec![
+            0xaf, 0x13, 0x49, 0xb9, 0xf5, 0xf9, 0xa1, 0xa6, 0xa0, 0x40, 0x4d, 0xea, 0x36, 0xdc,
+            0xc9, 0x49, 0x9b, 0xcb, 0x25, 0xc9, 0xad, 0xc1, 0x12, 0xb7, 0xcc, 0x9a, 0x93, 0xca,
+            0xe4, 0x1f, 0x32, 0x62
+        ])
+        .unwrap()
+    )
+}
+
+#[test]
+fn validate_empty() {
+    let d = Directory::default();
+    assert_eq!(d.validate(), Ok(()));
+}
+
+#[test]
+fn validate_invalid_names() {
+    {
+        let d = Directory {
+            directories: vec![DirectoryNode {
+                name: "".to_string(),
+                digest: DUMMY_DIGEST.to_vec(),
+                size: 42,
+            }],
+            ..Default::default()
+        };
+        match d.validate().expect_err("must fail") {
+            ValidateDirectoryError::InvalidName(n) => {
+                assert_eq!(n, "")
+            }
+            _ => panic!("unexpected error"),
+        };
+    }
+
+    {
+        let d = Directory {
+            directories: vec![DirectoryNode {
+                name: ".".to_string(),
+                digest: DUMMY_DIGEST.to_vec(),
+                size: 42,
+            }],
+            ..Default::default()
+        };
+        match d.validate().expect_err("must fail") {
+            ValidateDirectoryError::InvalidName(n) => {
+                assert_eq!(n, ".")
+            }
+            _ => panic!("unexpected error"),
+        };
+    }
+
+    {
+        let d = Directory {
+            files: vec![FileNode {
+                name: "..".to_string(),
+                digest: DUMMY_DIGEST.to_vec(),
+                size: 42,
+                executable: false,
+            }],
+            ..Default::default()
+        };
+        match d.validate().expect_err("must fail") {
+            ValidateDirectoryError::InvalidName(n) => {
+                assert_eq!(n, "..")
+            }
+            _ => panic!("unexpected error"),
+        };
+    }
+
+    {
+        let d = Directory {
+            symlinks: vec![SymlinkNode {
+                name: "\x00".to_string(),
+                target: "foo".to_string(),
+            }],
+            ..Default::default()
+        };
+        match d.validate().expect_err("must fail") {
+            ValidateDirectoryError::InvalidName(n) => {
+                assert_eq!(n, "\x00")
+            }
+            _ => panic!("unexpected error"),
+        };
+    }
+
+    {
+        let d = Directory {
+            symlinks: vec![SymlinkNode {
+                name: "foo/bar".to_string(),
+                target: "foo".to_string(),
+            }],
+            ..Default::default()
+        };
+        match d.validate().expect_err("must fail") {
+            ValidateDirectoryError::InvalidName(n) => {
+                assert_eq!(n, "foo/bar")
+            }
+            _ => panic!("unexpected error"),
+        };
+    }
+}
+
+#[test]
+fn validate_invalid_digest() {
+    let d = Directory {
+        directories: vec![DirectoryNode {
+            name: "foo".to_string(),
+            digest: vec![0x00, 0x42], // invalid length
+            size: 42,
+        }],
+        ..Default::default()
+    };
+    match d.validate().expect_err("must fail") {
+        ValidateDirectoryError::InvalidDigestLen(n) => {
+            assert_eq!(n, 2)
+        }
+        _ => panic!("unexpected error"),
+    }
+}
+
+#[test]
+fn validate_sorting() {
+    // "b" comes before "a", bad.
+    {
+        let d = Directory {
+            directories: vec![
+                DirectoryNode {
+                    name: "b".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+                DirectoryNode {
+                    name: "a".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+            ],
+            ..Default::default()
+        };
+        match d.validate().expect_err("must fail") {
+            ValidateDirectoryError::WrongSorting(s) => {
+                assert_eq!(s, "a".to_string());
+            }
+            _ => panic!("unexpected error"),
+        }
+    }
+
+    // "a" exists twice, bad.
+    {
+        let d = Directory {
+            directories: vec![
+                DirectoryNode {
+                    name: "a".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+                DirectoryNode {
+                    name: "a".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+            ],
+            ..Default::default()
+        };
+        match d.validate().expect_err("must fail") {
+            ValidateDirectoryError::DuplicateName(s) => {
+                assert_eq!(s, "a".to_string());
+            }
+            _ => panic!("unexpected error"),
+        }
+    }
+
+    // "a" comes before "b", all good.
+    {
+        let d = Directory {
+            directories: vec![
+                DirectoryNode {
+                    name: "a".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+                DirectoryNode {
+                    name: "b".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+            ],
+            ..Default::default()
+        };
+
+        d.validate().expect("validate shouldn't error");
+    }
+
+    // [b, c] and [a] are both properly sorted.
+    {
+        let d = Directory {
+            directories: vec![
+                DirectoryNode {
+                    name: "b".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+                DirectoryNode {
+                    name: "c".to_string(),
+                    digest: DUMMY_DIGEST.to_vec(),
+                    size: 42,
+                },
+            ],
+            symlinks: vec![SymlinkNode {
+                name: "a".to_string(),
+                target: "foo".to_string(),
+            }],
+            ..Default::default()
+        };
+
+        d.validate().expect("validate shouldn't error");
+    }
+}
diff --git a/tvix/store/src/proto/tests/directory_nodes_iterator.rs b/tvix/store/src/proto/tests/directory_nodes_iterator.rs
new file mode 100644
index 000000000000..9a283f72bd45
--- /dev/null
+++ b/tvix/store/src/proto/tests/directory_nodes_iterator.rs
@@ -0,0 +1,80 @@
+use crate::proto::node::Node;
+use crate::proto::Directory;
+use crate::proto::DirectoryNode;
+use crate::proto::FileNode;
+use crate::proto::SymlinkNode;
+
+#[test]
+fn iterator() {
+    let d = Directory {
+        directories: vec![
+            DirectoryNode {
+                name: "c".to_string(),
+                ..DirectoryNode::default()
+            },
+            DirectoryNode {
+                name: "d".to_string(),
+                ..DirectoryNode::default()
+            },
+            DirectoryNode {
+                name: "h".to_string(),
+                ..DirectoryNode::default()
+            },
+            DirectoryNode {
+                name: "l".to_string(),
+                ..DirectoryNode::default()
+            },
+        ],
+        files: vec![
+            FileNode {
+                name: "b".to_string(),
+                ..FileNode::default()
+            },
+            FileNode {
+                name: "e".to_string(),
+                ..FileNode::default()
+            },
+            FileNode {
+                name: "g".to_string(),
+                ..FileNode::default()
+            },
+            FileNode {
+                name: "j".to_string(),
+                ..FileNode::default()
+            },
+        ],
+        symlinks: vec![
+            SymlinkNode {
+                name: "a".to_string(),
+                ..SymlinkNode::default()
+            },
+            SymlinkNode {
+                name: "f".to_string(),
+                ..SymlinkNode::default()
+            },
+            SymlinkNode {
+                name: "i".to_string(),
+                ..SymlinkNode::default()
+            },
+            SymlinkNode {
+                name: "k".to_string(),
+                ..SymlinkNode::default()
+            },
+        ],
+    };
+
+    let mut node_names: Vec<String> = vec![];
+
+    for node in d.nodes() {
+        match node {
+            Node::Directory(n) => node_names.push(n.name),
+            Node::File(n) => node_names.push(n.name),
+            Node::Symlink(n) => node_names.push(n.name),
+        };
+    }
+
+    assert_eq!(
+        vec!["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"],
+        node_names
+    );
+}
diff --git a/tvix/store/src/proto/tests/grpc_blobservice.rs b/tvix/store/src/proto/tests/grpc_blobservice.rs
new file mode 100644
index 000000000000..02e04e7d723f
--- /dev/null
+++ b/tvix/store/src/proto/tests/grpc_blobservice.rs
@@ -0,0 +1,102 @@
+use crate::blobservice::BlobService;
+use crate::proto::blob_service_server::BlobService as GRPCBlobService;
+use crate::proto::{BlobChunk, GRPCBlobServiceWrapper, ReadBlobRequest, StatBlobRequest};
+use crate::tests::fixtures::{BLOB_A, BLOB_A_DIGEST};
+use crate::tests::utils::gen_blob_service;
+use tokio_stream::StreamExt;
+
+fn gen_grpc_blob_service(
+) -> GRPCBlobServiceWrapper<impl BlobService + Send + Sync + Clone + 'static> {
+    let blob_service = gen_blob_service();
+    GRPCBlobServiceWrapper::from(blob_service)
+}
+
+/// Trying to read a non-existent blob should return a not found error.
+#[tokio::test]
+async fn not_found_read() {
+    let service = gen_grpc_blob_service();
+
+    let resp = service
+        .read(tonic::Request::new(ReadBlobRequest {
+            digest: BLOB_A_DIGEST.to_vec(),
+        }))
+        .await;
+
+    // We can't use unwrap_err here, because the Ok value doesn't implement
+    // debug.
+    if let Err(e) = resp {
+        assert_eq!(e.code(), tonic::Code::NotFound);
+    } else {
+        panic!("resp is not err")
+    }
+}
+
+/// Trying to stat a non-existent blob should return a not found error.
+#[tokio::test]
+async fn not_found_stat() {
+    let service = gen_grpc_blob_service();
+
+    let resp = service
+        .stat(tonic::Request::new(StatBlobRequest {
+            digest: BLOB_A_DIGEST.to_vec(),
+            ..Default::default()
+        }))
+        .await
+        .expect_err("must fail");
+
+    // The resp should be a status with Code::NotFound
+    assert_eq!(resp.code(), tonic::Code::NotFound);
+}
+
+/// Put a blob in the store, get it back.
+#[tokio::test]
+async fn put_read_stat() {
+    let service = gen_grpc_blob_service();
+
+    // Send blob A.
+    let put_resp = service
+        .put(tonic_mock::streaming_request(vec![BlobChunk {
+            data: BLOB_A.clone(),
+        }]))
+        .await
+        .expect("must succeed")
+        .into_inner();
+
+    assert_eq!(BLOB_A_DIGEST.to_vec(), put_resp.digest);
+
+    // Stat for the digest of A.
+    // We currently don't ask for more granular chunking data, as we don't
+    // expose it yet.
+    let _resp = service
+        .stat(tonic::Request::new(StatBlobRequest {
+            digest: BLOB_A_DIGEST.to_vec(),
+            ..Default::default()
+        }))
+        .await
+        .expect("must succeed")
+        .into_inner();
+
+    // Read the blob. It should return the same data.
+    let resp = service
+        .read(tonic::Request::new(ReadBlobRequest {
+            digest: BLOB_A_DIGEST.to_vec(),
+        }))
+        .await;
+
+    let mut rx = resp.ok().unwrap().into_inner();
+
+    // the stream should contain one element, a BlobChunk with the same contents as BLOB_A.
+    let item = rx
+        .next()
+        .await
+        .expect("must be some")
+        .expect("must succeed");
+
+    assert_eq!(BLOB_A.to_vec(), item.data);
+
+    // … and no more elements
+    assert!(rx.next().await.is_none());
+
+    // TODO: we rely here on the blob being small enough to not get broken up into multiple chunks.
+    // Test with some bigger blob too
+}
diff --git a/tvix/store/src/proto/tests/grpc_directoryservice.rs b/tvix/store/src/proto/tests/grpc_directoryservice.rs
new file mode 100644
index 000000000000..069e82f6463e
--- /dev/null
+++ b/tvix/store/src/proto/tests/grpc_directoryservice.rs
@@ -0,0 +1,241 @@
+use crate::directoryservice::DirectoryService;
+use crate::proto::directory_service_server::DirectoryService as GRPCDirectoryService;
+use crate::proto::get_directory_request::ByWhat;
+use crate::proto::{Directory, DirectoryNode, SymlinkNode};
+use crate::proto::{GRPCDirectoryServiceWrapper, GetDirectoryRequest};
+use crate::tests::fixtures::{DIRECTORY_A, DIRECTORY_B, DIRECTORY_C};
+use crate::tests::utils::gen_directory_service;
+use tokio_stream::StreamExt;
+use tonic::Status;
+
+fn gen_grpc_service(
+) -> GRPCDirectoryServiceWrapper<impl DirectoryService + Send + Sync + Clone + 'static> {
+    let directory_service = gen_directory_service();
+    GRPCDirectoryServiceWrapper::from(directory_service)
+}
+
+/// Send the specified GetDirectoryRequest.
+/// Returns an error in the case of an error response, or an error in one of
+// the items in the stream, or a Vec<Directory> in the case of a successful
+/// request.
+async fn get_directories<S: GRPCDirectoryService>(
+    svc: &S,
+    get_directory_request: GetDirectoryRequest,
+) -> Result<Vec<Directory>, Status> {
+    let resp = svc.get(tonic::Request::new(get_directory_request)).await;
+
+    // if the response is an error itself, return the error, otherwise unpack
+    let stream = match resp {
+        Ok(resp) => resp,
+        Err(status) => return Err(status),
+    }
+    .into_inner();
+
+    let directory_results: Vec<Result<Directory, Status>> = stream.collect().await;
+
+    // turn Vec<Result<Directory, Status> into Result<Vec<Directory>,Status>
+    directory_results.into_iter().collect()
+}
+
+/// Trying to get a non-existent Directory should return a not found error.
+#[tokio::test]
+async fn not_found() {
+    let service = gen_grpc_service();
+
+    let resp = service
+        .get(tonic::Request::new(GetDirectoryRequest {
+            by_what: Some(ByWhat::Digest(DIRECTORY_A.digest().to_vec())),
+            ..Default::default()
+        }))
+        .await;
+
+    let mut rx = resp.expect("must succeed").into_inner().into_inner();
+
+    // The stream should contain one element, an error with Code::NotFound.
+    let item = rx
+        .recv()
+        .await
+        .expect("must be some")
+        .expect_err("must be err");
+    assert_eq!(item.code(), tonic::Code::NotFound);
+
+    // … and nothing else
+    assert!(rx.recv().await.is_none());
+}
+
+/// Put a Directory into the store, get it back.
+#[tokio::test]
+async fn put_get() {
+    let service = gen_grpc_service();
+
+    let streaming_request = tonic_mock::streaming_request(vec![DIRECTORY_A.clone()]);
+    let put_resp = service
+        .put(streaming_request)
+        .await
+        .expect("must succeed")
+        .into_inner();
+
+    // the sent root_digest should match the calculated digest
+    assert_eq!(put_resp.root_digest, DIRECTORY_A.digest().to_vec());
+
+    // get it back
+    let items = get_directories(
+        &service,
+        GetDirectoryRequest {
+            by_what: Some(ByWhat::Digest(DIRECTORY_A.digest().to_vec())),
+            ..Default::default()
+        },
+    )
+    .await
+    .expect("must not error");
+
+    assert_eq!(vec![DIRECTORY_A.clone()], items);
+}
+
+/// Put multiple Directories into the store, and get them back
+#[tokio::test]
+async fn put_get_multiple() {
+    let service = gen_grpc_service();
+
+    // sending "b" (which refers to "a") without sending "a" first should fail.
+    let put_resp = service
+        .put(tonic_mock::streaming_request(vec![DIRECTORY_B.clone()]))
+        .await
+        .expect_err("must fail");
+
+    assert_eq!(tonic::Code::InvalidArgument, put_resp.code());
+
+    // sending "a", then "b" should succeed, and the response should contain the digest of b.
+    let put_resp = service
+        .put(tonic_mock::streaming_request(vec![
+            DIRECTORY_A.clone(),
+            DIRECTORY_B.clone(),
+        ]))
+        .await
+        .expect("must succeed");
+
+    assert_eq!(
+        DIRECTORY_B.digest().to_vec(),
+        put_resp.into_inner().root_digest
+    );
+
+    // now, request b, first in non-recursive mode.
+    let items = get_directories(
+        &service,
+        GetDirectoryRequest {
+            recursive: false,
+            by_what: Some(ByWhat::Digest(DIRECTORY_B.digest().to_vec())),
+        },
+    )
+    .await
+    .expect("must not error");
+
+    // We expect to only get b.
+    assert_eq!(vec![DIRECTORY_B.clone()], items);
+
+    // now, request b, but in recursive mode.
+    let items = get_directories(
+        &service,
+        GetDirectoryRequest {
+            recursive: true,
+            by_what: Some(ByWhat::Digest(DIRECTORY_B.digest().to_vec())),
+        },
+    )
+    .await
+    .expect("must not error");
+
+    // We expect to get b, and then a, because that's how we traverse down.
+    assert_eq!(vec![DIRECTORY_B.clone(), DIRECTORY_A.clone()], items);
+}
+
+/// Put multiple Directories into the store, and omit duplicates.
+#[tokio::test]
+async fn put_get_dedup() {
+    let service = gen_grpc_service();
+
+    // Send "A", then "C", which refers to "A" two times
+    // Pretend we're a dumb client sending A twice.
+    let put_resp = service
+        .put(tonic_mock::streaming_request(vec![
+            DIRECTORY_A.clone(),
+            DIRECTORY_A.clone(),
+            DIRECTORY_C.clone(),
+        ]))
+        .await
+        .expect("must succeed");
+
+    assert_eq!(
+        DIRECTORY_C.digest().to_vec(),
+        put_resp.into_inner().root_digest
+    );
+
+    // Ask for "C" recursively. We expect to only get "A" once, as there's no point sending it twice.
+    let items = get_directories(
+        &service,
+        GetDirectoryRequest {
+            recursive: true,
+            by_what: Some(ByWhat::Digest(DIRECTORY_C.digest().to_vec())),
+        },
+    )
+    .await
+    .expect("must not error");
+
+    // We expect to get C, and then A (once, as the second A has been deduplicated).
+    assert_eq!(vec![DIRECTORY_C.clone(), DIRECTORY_A.clone()], items);
+}
+
+/// Trying to upload a Directory failing validation should fail.
+#[tokio::test]
+async fn put_reject_failed_validation() {
+    let service = gen_grpc_service();
+
+    // construct a broken Directory message that fails validation
+    let broken_directory = Directory {
+        symlinks: vec![SymlinkNode {
+            name: "".to_string(),
+            target: "doesntmatter".to_string(),
+        }],
+        ..Default::default()
+    };
+    assert!(broken_directory.validate().is_err());
+
+    // send it over, it must fail
+    let put_resp = service
+        .put(tonic_mock::streaming_request(vec![broken_directory]))
+        .await
+        .expect_err("must fail");
+
+    assert_eq!(put_resp.code(), tonic::Code::InvalidArgument);
+}
+
+/// Trying to upload a Directory with wrong size should fail.
+#[tokio::test]
+async fn put_reject_wrong_size() {
+    let service = gen_grpc_service();
+
+    // Construct a directory referring to DIRECTORY_A, but with wrong size.
+    let broken_parent_directory = Directory {
+        directories: vec![DirectoryNode {
+            name: "foo".to_string(),
+            digest: DIRECTORY_A.digest().to_vec(),
+            size: 42,
+        }],
+        ..Default::default()
+    };
+    // Make sure we got the size wrong.
+    assert_ne!(
+        broken_parent_directory.directories[0].size,
+        DIRECTORY_A.size()
+    );
+
+    // now upload both (first A, then the broken parent). This must fail.
+    let put_resp = service
+        .put(tonic_mock::streaming_request(vec![
+            DIRECTORY_A.clone(),
+            broken_parent_directory,
+        ]))
+        .await
+        .expect_err("must fail");
+
+    assert_eq!(put_resp.code(), tonic::Code::InvalidArgument);
+}
diff --git a/tvix/store/src/proto/tests/grpc_pathinfoservice.rs b/tvix/store/src/proto/tests/grpc_pathinfoservice.rs
new file mode 100644
index 000000000000..11cab2c264cc
--- /dev/null
+++ b/tvix/store/src/proto/tests/grpc_pathinfoservice.rs
@@ -0,0 +1,67 @@
+use crate::nar::NonCachingNARCalculationService;
+use crate::proto::get_path_info_request::ByWhat::ByOutputHash;
+use crate::proto::node::Node::Symlink;
+use crate::proto::path_info_service_server::PathInfoService as GRPCPathInfoService;
+use crate::proto::GRPCPathInfoServiceWrapper;
+use crate::proto::PathInfo;
+use crate::proto::{GetPathInfoRequest, Node, SymlinkNode};
+use crate::tests::fixtures::DUMMY_OUTPUT_HASH;
+use crate::tests::utils::{gen_blob_service, gen_directory_service, gen_pathinfo_service};
+use tonic::Request;
+
+/// generates a GRPCPathInfoService out of blob, directory and pathinfo services.
+///
+/// We only interact with it via the PathInfo GRPC interface.
+/// It uses the NonCachingNARCalculationService NARCalculationService to
+/// calculate NARs.
+fn gen_grpc_service() -> impl GRPCPathInfoService {
+    GRPCPathInfoServiceWrapper::new(
+        gen_pathinfo_service(),
+        NonCachingNARCalculationService::new(gen_blob_service(), gen_directory_service()),
+    )
+}
+
+/// Trying to get a non-existent PathInfo should return a not found error.
+#[tokio::test]
+async fn not_found() {
+    let service = gen_grpc_service();
+
+    let resp = service
+        .get(Request::new(GetPathInfoRequest {
+            by_what: Some(ByOutputHash(DUMMY_OUTPUT_HASH.to_vec())),
+        }))
+        .await;
+
+    let resp = resp.expect_err("must fail");
+    assert_eq!(resp.code(), tonic::Code::NotFound);
+}
+
+/// Put a PathInfo into the store, get it back.
+#[tokio::test]
+async fn put_get() {
+    let service = gen_grpc_service();
+
+    let path_info = PathInfo {
+        node: Some(Node {
+            node: Some(Symlink(SymlinkNode {
+                name: "00000000000000000000000000000000-foo".to_string(),
+                target: "doesntmatter".to_string(),
+            })),
+        }),
+        ..Default::default()
+    };
+
+    let resp = service.put(Request::new(path_info.clone())).await;
+
+    assert!(resp.is_ok());
+    assert_eq!(resp.expect("must succeed").into_inner(), path_info);
+
+    let resp = service
+        .get(Request::new(GetPathInfoRequest {
+            by_what: Some(ByOutputHash(DUMMY_OUTPUT_HASH.to_vec())),
+        }))
+        .await;
+
+    assert!(resp.is_ok());
+    assert_eq!(resp.expect("must succeed").into_inner(), path_info);
+}
diff --git a/tvix/store/src/proto/tests/mod.rs b/tvix/store/src/proto/tests/mod.rs
new file mode 100644
index 000000000000..0a96ea3a0d59
--- /dev/null
+++ b/tvix/store/src/proto/tests/mod.rs
@@ -0,0 +1,6 @@
+mod directory;
+mod directory_nodes_iterator;
+mod grpc_blobservice;
+mod grpc_directoryservice;
+mod grpc_pathinfoservice;
+mod pathinfo;
diff --git a/tvix/store/src/proto/tests/pathinfo.rs b/tvix/store/src/proto/tests/pathinfo.rs
new file mode 100644
index 000000000000..54a76fc6c554
--- /dev/null
+++ b/tvix/store/src/proto/tests/pathinfo.rs
@@ -0,0 +1,207 @@
+use crate::proto::{self, Node, PathInfo, ValidatePathInfoError};
+use lazy_static::lazy_static;
+use nix_compat::store_path::{self, StorePath};
+use test_case::test_case;
+
+lazy_static! {
+    static ref DUMMY_DIGEST: Vec<u8> = vec![
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00,
+    ];
+    static ref DUMMY_DIGEST_2: Vec<u8> = vec![
+        0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00,
+    ];
+}
+
+const DUMMY_NAME: &str = "00000000000000000000000000000000-dummy";
+
+#[test_case(
+    None,
+    Err(ValidatePathInfoError::NoNodePresent()) ;
+    "No node"
+)]
+#[test_case(
+    Some(Node { node: None }),
+    Err(ValidatePathInfoError::NoNodePresent());
+    "No node 2"
+)]
+fn validate_no_node(
+    t_node: Option<proto::Node>,
+    t_result: Result<StorePath, ValidatePathInfoError>,
+) {
+    // construct the PathInfo object
+    let p = PathInfo {
+        node: t_node,
+        ..Default::default()
+    };
+    assert_eq!(t_result, p.validate());
+}
+
+#[test_case(
+    proto::DirectoryNode {
+        name: DUMMY_NAME.to_string(),
+        digest: DUMMY_DIGEST.to_vec(),
+        size: 0,
+    },
+    Ok(StorePath::from_string(DUMMY_NAME).expect("must succeed"));
+    "ok"
+)]
+#[test_case(
+    proto::DirectoryNode {
+        name: DUMMY_NAME.to_string(),
+        digest: vec![],
+        size: 0,
+    },
+    Err(ValidatePathInfoError::InvalidDigestLen(0));
+    "invalid digest length"
+)]
+#[test_case(
+    proto::DirectoryNode {
+        name: "invalid".to_string(),
+        digest: DUMMY_DIGEST.to_vec(),
+        size: 0,
+    },
+    Err(ValidatePathInfoError::InvalidNodeName(
+        "invalid".to_string(),
+        store_path::Error::InvalidName(store_path::NameError::InvalidName("".to_string()))
+    ));
+    "invalid node name"
+)]
+fn validate_directory(
+    t_directory_node: proto::DirectoryNode,
+    t_result: Result<StorePath, ValidatePathInfoError>,
+) {
+    // construct the PathInfo object
+    let p = PathInfo {
+        node: Some(Node {
+            node: Some(proto::node::Node::Directory(t_directory_node)),
+        }),
+        ..Default::default()
+    };
+    assert_eq!(t_result, p.validate());
+}
+
+#[test_case(
+    proto::FileNode {
+        name: DUMMY_NAME.to_string(),
+        digest: DUMMY_DIGEST.to_vec(),
+        size: 0,
+        executable: false,
+    },
+    Ok(StorePath::from_string(DUMMY_NAME).expect("must succeed"));
+    "ok"
+)]
+#[test_case(
+    proto::FileNode {
+        name: DUMMY_NAME.to_string(),
+        digest: vec![],
+        ..Default::default()
+    },
+    Err(ValidatePathInfoError::InvalidDigestLen(0));
+    "invalid digest length"
+)]
+#[test_case(
+    proto::FileNode {
+        name: "invalid".to_string(),
+        digest: DUMMY_DIGEST.to_vec(),
+        ..Default::default()
+    },
+    Err(ValidatePathInfoError::InvalidNodeName(
+        "invalid".to_string(),
+        store_path::Error::InvalidName(store_path::NameError::InvalidName("".to_string()))
+    ));
+    "invalid node name"
+)]
+fn validate_file(t_file_node: proto::FileNode, t_result: Result<StorePath, ValidatePathInfoError>) {
+    // construct the PathInfo object
+    let p = PathInfo {
+        node: Some(Node {
+            node: Some(proto::node::Node::File(t_file_node)),
+        }),
+        ..Default::default()
+    };
+    assert_eq!(t_result, p.validate());
+}
+
+#[test_case(
+    proto::SymlinkNode {
+        name: DUMMY_NAME.to_string(),
+        ..Default::default()
+    },
+    Ok(StorePath::from_string(DUMMY_NAME).expect("must succeed"));
+    "ok"
+)]
+#[test_case(
+    proto::SymlinkNode {
+        name: "invalid".to_string(),
+        ..Default::default()
+    },
+    Err(ValidatePathInfoError::InvalidNodeName(
+        "invalid".to_string(),
+        store_path::Error::InvalidName(store_path::NameError::InvalidName("".to_string()))
+    ));
+    "invalid node name"
+)]
+fn validate_symlink(
+    t_symlink_node: proto::SymlinkNode,
+    t_result: Result<StorePath, ValidatePathInfoError>,
+) {
+    // construct the PathInfo object
+    let p = PathInfo {
+        node: Some(Node {
+            node: Some(proto::node::Node::Symlink(t_symlink_node)),
+        }),
+        ..Default::default()
+    };
+    assert_eq!(t_result, p.validate());
+}
+
+#[test]
+fn validate_references() {
+    // create a PathInfo without narinfo field.
+    let path_info = PathInfo {
+        node: Some(Node {
+            node: Some(proto::node::Node::Directory(proto::DirectoryNode {
+                name: DUMMY_NAME.to_string(),
+                digest: DUMMY_DIGEST.to_vec(),
+                size: 0,
+            })),
+        }),
+        references: vec![DUMMY_DIGEST_2.to_vec()],
+        narinfo: None,
+    };
+    assert!(path_info.validate().is_ok());
+
+    // create a PathInfo with a narinfo field, but an inconsistent set of references
+    let path_info_with_narinfo_missing_refs = PathInfo {
+        narinfo: Some(proto::NarInfo {
+            nar_size: 0,
+            nar_sha256: DUMMY_DIGEST.to_vec(),
+            signatures: vec![],
+            reference_names: vec![],
+        }),
+        ..path_info.clone()
+    };
+    match path_info_with_narinfo_missing_refs
+        .validate()
+        .expect_err("must_fail")
+    {
+        ValidatePathInfoError::InconsistentNumberOfReferences(_, _) => {}
+        _ => panic!("unexpected error"),
+    };
+
+    // create a pathinfo with the correct number of references, should suceed
+    let path_info_with_narinfo = PathInfo {
+        narinfo: Some(proto::NarInfo {
+            nar_size: 0,
+            nar_sha256: DUMMY_DIGEST.to_vec(),
+            signatures: vec![],
+            reference_names: vec![format!("/nix/store/{}", DUMMY_NAME)],
+        }),
+        ..path_info
+    };
+    assert!(path_info_with_narinfo.validate().is_ok());
+}
diff --git a/tvix/store/src/store_io.rs b/tvix/store/src/store_io.rs
new file mode 100644
index 000000000000..fb46204e505f
--- /dev/null
+++ b/tvix/store/src/store_io.rs
@@ -0,0 +1,358 @@
+//! This module provides an implementation of EvalIO.
+//!
+//! It can be used by the tvix evalutator to talk to a tvix store.
+
+use data_encoding::BASE64;
+use nix_compat::{
+    nixhash::{HashAlgo, NixHash, NixHashWithMode},
+    store_path::{build_regular_ca_path, StorePath},
+};
+use smol_str::SmolStr;
+use std::{io, path::Path, path::PathBuf};
+use tracing::{error, instrument, warn};
+use tvix_eval::{EvalIO, FileType, StdIO};
+
+use crate::{
+    blobservice::BlobService,
+    directoryservice::{self, DirectoryService},
+    import,
+    nar::NARCalculationService,
+    pathinfoservice::PathInfoService,
+    proto::NamedNode,
+    B3Digest,
+};
+
+/// Implements [EvalIO], asking given [PathInfoService], [DirectoryService]
+/// and [BlobService].
+///
+/// In case the given path does not exist in these stores, we ask StdIO.
+/// This is to both cover cases of syntactically valid store paths, that exist
+/// on the filesystem (still managed by Nix), as well as being able to read
+/// files outside store paths.
+pub struct TvixStoreIO<
+    BS: BlobService,
+    DS: DirectoryService,
+    PS: PathInfoService,
+    NCS: NARCalculationService,
+> {
+    blob_service: BS,
+    directory_service: DS,
+    path_info_service: PS,
+    nar_calculation_service: NCS,
+    std_io: StdIO,
+}
+
+impl<BS: BlobService, DS: DirectoryService, PS: PathInfoService, NCS: NARCalculationService>
+    TvixStoreIO<BS, DS, PS, NCS>
+{
+    pub fn new(
+        blob_service: BS,
+        directory_service: DS,
+        path_info_service: PS,
+        nar_calculation_service: NCS,
+    ) -> Self {
+        Self {
+            blob_service,
+            directory_service,
+            path_info_service,
+            nar_calculation_service,
+            std_io: StdIO {},
+        }
+    }
+
+    /// for a given [StorePath] and additional [Path] inside the store path,
+    /// look up the [PathInfo], and if it exists, traverse the directory structure to
+    /// return the [crate::proto::node::Node] specified by `sub_path`.
+    #[instrument(skip(self), ret, err)]
+    fn store_path_to_root_node(
+        &self,
+        store_path: &StorePath,
+        sub_path: &Path,
+    ) -> Result<Option<crate::proto::node::Node>, crate::Error> {
+        let path_info = {
+            match self.path_info_service.get(store_path.digest)? {
+                // If there's no PathInfo found, early exit
+                None => return Ok(None),
+                Some(path_info) => path_info,
+            }
+        };
+
+        let root_node = {
+            match path_info.node {
+                None => {
+                    warn!(
+                        "returned PathInfo {:?} node is None, this shouldn't happen.",
+                        &path_info
+                    );
+                    return Ok(None);
+                }
+                Some(root_node) => match root_node.node {
+                    None => {
+                        warn!("node for {:?} is None, this shouldn't happen.", &root_node);
+                        return Ok(None);
+                    }
+                    Some(root_node) => root_node,
+                },
+            }
+        };
+
+        directoryservice::traverse_to(&self.directory_service, root_node, sub_path)
+    }
+
+    /// Imports a given path on the filesystem into the store, and returns the
+    /// [crate::proto::PathInfo] describing the path, that was sent to
+    /// [PathInfoService].
+    /// While not part of the [EvalIO], it's still useful for clients who
+    /// care about the [PathInfo].
+    #[instrument(skip(self), ret, err)]
+    pub fn import_path_with_pathinfo(
+        &self,
+        path: &std::path::Path,
+    ) -> Result<crate::proto::PathInfo, io::Error> {
+        // Call [import::ingest_path], which will walk over the given path and return a root_node.
+        let root_node = import::ingest_path(&self.blob_service, &self.directory_service, path)
+            .expect("error during import_path");
+
+        // Render the NAR
+        let (nar_size, nar_sha256) = self
+            .nar_calculation_service
+            .calculate_nar(&root_node)
+            .expect("error during nar calculation"); // TODO: handle error
+
+        // For given NAR sha256 digest and name, return the new [StorePath] this would have.
+        let nar_hash_with_mode =
+            NixHashWithMode::Recursive(NixHash::new(HashAlgo::Sha256, nar_sha256.to_vec()));
+
+        let name = path
+            .file_name()
+            .expect("path must not be ..")
+            .to_str()
+            .expect("path must be valid unicode");
+
+        let output_path =
+            build_regular_ca_path(name, &nar_hash_with_mode, Vec::<String>::new(), false).unwrap();
+
+        // assemble a new root_node with a name that is derived from the nar hash.
+        let renamed_root_node = {
+            let name = output_path.to_string();
+
+            match root_node {
+                crate::proto::node::Node::Directory(n) => {
+                    crate::proto::node::Node::Directory(crate::proto::DirectoryNode { name, ..n })
+                }
+                crate::proto::node::Node::File(n) => {
+                    crate::proto::node::Node::File(crate::proto::FileNode { name, ..n })
+                }
+                crate::proto::node::Node::Symlink(n) => {
+                    crate::proto::node::Node::Symlink(crate::proto::SymlinkNode { name, ..n })
+                }
+            }
+        };
+
+        // assemble the [crate::proto::PathInfo] object.
+        let path_info = crate::proto::PathInfo {
+            node: Some(crate::proto::Node {
+                node: Some(renamed_root_node),
+            }),
+            // There's no reference scanning on path contents ingested like this.
+            references: vec![],
+            narinfo: Some(crate::proto::NarInfo {
+                nar_size,
+                nar_sha256: nar_sha256.to_vec(),
+                signatures: vec![],
+                reference_names: vec![],
+                // TODO: narinfo for talosctl.src contains `CA: fixed:r:sha256:1x13j5hy75221bf6kz7cpgld9vgic6bqx07w5xjs4pxnksj6lxb6`
+                // do we need this anywhere?
+            }),
+        };
+
+        // put into [PathInfoService], and return the PathInfo that we get back
+        // from there (it might contain additional signatures).
+        let path_info = self.path_info_service.put(path_info)?;
+
+        Ok(path_info)
+    }
+}
+
+/// For given NAR sha256 digest and name, return the new [StorePath] this would have.
+#[instrument(skip(nar_sha256_digest), ret, fields(nar_sha256_digest=BASE64.encode(nar_sha256_digest)))]
+fn calculate_nar_based_store_path(nar_sha256_digest: &[u8; 32], name: &str) -> StorePath {
+    let nar_hash_with_mode =
+        NixHashWithMode::Recursive(NixHash::new(HashAlgo::Sha256, nar_sha256_digest.to_vec()));
+
+    build_regular_ca_path(name, &nar_hash_with_mode, Vec::<String>::new(), false).unwrap()
+}
+
+impl<BS: BlobService, DS: DirectoryService, PS: PathInfoService, NCS: NARCalculationService> EvalIO
+    for TvixStoreIO<BS, DS, PS, NCS>
+{
+    #[instrument(skip(self), ret, err)]
+    fn path_exists(&self, path: &Path) -> Result<bool, io::Error> {
+        if let Ok((store_path, sub_path)) =
+            StorePath::from_absolute_path_full(&path.to_string_lossy())
+        {
+            if self
+                .store_path_to_root_node(&store_path, &sub_path)?
+                .is_some()
+            {
+                Ok(true)
+            } else {
+                // As tvix-store doesn't manage /nix/store on the filesystem,
+                // we still need to also ask self.std_io here.
+                self.std_io.path_exists(path)
+            }
+        } else {
+            // The store path is no store path, so do regular StdIO.
+            self.std_io.path_exists(path)
+        }
+    }
+
+    #[instrument(skip(self), ret, err)]
+    fn read_to_string(&self, path: &Path) -> Result<String, io::Error> {
+        if let Ok((store_path, sub_path)) =
+            StorePath::from_absolute_path_full(&path.to_string_lossy())
+        {
+            if let Some(node) = self.store_path_to_root_node(&store_path, &sub_path)? {
+                // depending on the node type, treat read_to_string differently
+                match node {
+                    crate::proto::node::Node::Directory(_) => {
+                        // This would normally be a io::ErrorKind::IsADirectory (still unstable)
+                        Err(io::Error::new(
+                            io::ErrorKind::Unsupported,
+                            "tried to read directory at {path} to string",
+                        ))
+                    }
+                    crate::proto::node::Node::File(file_node) => {
+                        let digest =
+                            B3Digest::from_vec(file_node.digest.clone()).map_err(|_e| {
+                                error!(
+                                    file_node = ?file_node,
+                                    "invalid digest"
+                                );
+                                io::Error::new(
+                                    io::ErrorKind::InvalidData,
+                                    format!("invalid digest length in file node: {:?}", file_node),
+                                )
+                            })?;
+
+                        let reader = {
+                            let resp = self.blob_service.open_read(&digest)?;
+                            match resp {
+                                Some(blob_reader) => blob_reader,
+                                None => {
+                                    error!(
+                                        blob.digest = %digest,
+                                        "blob not found",
+                                    );
+                                    Err(io::Error::new(
+                                        io::ErrorKind::NotFound,
+                                        format!("blob {} not found", &digest),
+                                    ))?
+                                }
+                            }
+                        };
+
+                        io::read_to_string(reader)
+                    }
+                    crate::proto::node::Node::Symlink(_symlink_node) => Err(io::Error::new(
+                        io::ErrorKind::Unsupported,
+                        "read_to_string for symlinks is unsupported",
+                    ))?,
+                }
+            } else {
+                // As tvix-store doesn't manage /nix/store on the filesystem,
+                // we still need to also ask self.std_io here.
+                self.std_io.read_to_string(path)
+            }
+        } else {
+            // The store path is no store path, so do regular StdIO.
+            self.std_io.read_to_string(path)
+        }
+    }
+
+    #[instrument(skip(self), ret, err)]
+    fn read_dir(&self, path: &Path) -> Result<Vec<(SmolStr, FileType)>, io::Error> {
+        if let Ok((store_path, sub_path)) =
+            StorePath::from_absolute_path_full(&path.to_string_lossy())
+        {
+            if let Some(node) = self.store_path_to_root_node(&store_path, &sub_path)? {
+                match node {
+                    crate::proto::node::Node::Directory(directory_node) => {
+                        // fetch the Directory itself.
+                        let digest =
+                            B3Digest::from_vec(directory_node.digest.clone()).map_err(|_e| {
+                                io::Error::new(
+                                    io::ErrorKind::InvalidData,
+                                    format!(
+                                        "invalid digest length in directory node: {:?}",
+                                        directory_node
+                                    ),
+                                )
+                            })?;
+
+                        if let Some(directory) = self.directory_service.get(&digest)? {
+                            let mut children: Vec<(SmolStr, FileType)> = Vec::new();
+                            for node in directory.nodes() {
+                                children.push(match node {
+                                    crate::proto::node::Node::Directory(e) => {
+                                        (e.name.into(), FileType::Directory)
+                                    }
+                                    crate::proto::node::Node::File(e) => {
+                                        (e.name.into(), FileType::Regular)
+                                    }
+                                    crate::proto::node::Node::Symlink(e) => {
+                                        (e.name.into(), FileType::Symlink)
+                                    }
+                                })
+                            }
+                            Ok(children)
+                        } else {
+                            // If we didn't get the directory node that's linked, that's a store inconsistency!
+                            error!(
+                                directory.digest = %digest,
+                                path = ?path,
+                                "directory not found",
+                            );
+                            Err(io::Error::new(
+                                io::ErrorKind::NotFound,
+                                format!("directory {digest} does not exist"),
+                            ))?
+                        }
+                    }
+                    crate::proto::node::Node::File(_file_node) => {
+                        // This would normally be a io::ErrorKind::NotADirectory (still unstable)
+                        Err(io::Error::new(
+                            io::ErrorKind::Unsupported,
+                            "tried to readdir path {:?}, which is a file",
+                        ))?
+                    }
+                    crate::proto::node::Node::Symlink(_symlink_node) => Err(io::Error::new(
+                        io::ErrorKind::Unsupported,
+                        "read_dir for symlinks is unsupported",
+                    ))?,
+                }
+            } else {
+                self.std_io.read_dir(path)
+            }
+        } else {
+            self.std_io.read_dir(path)
+        }
+    }
+
+    #[instrument(skip(self), ret, err)]
+    fn import_path(&self, path: &std::path::Path) -> Result<PathBuf, std::io::Error> {
+        let path_info = self.import_path_with_pathinfo(path)?;
+
+        // from the [PathInfo], extract the store path (as string).
+        let mut path = PathBuf::from(nix_compat::store_path::STORE_DIR_WITH_SLASH);
+        path.push(path_info.node.unwrap().node.unwrap().get_name());
+
+        // and return it
+        Ok(path)
+    }
+
+    #[instrument(skip(self), ret)]
+    fn store_dir(&self) -> Option<String> {
+        Some("/nix/store".to_string())
+    }
+}
diff --git a/tvix/store/src/tests/fixtures.rs b/tvix/store/src/tests/fixtures.rs
new file mode 100644
index 000000000000..934d9e4c5302
--- /dev/null
+++ b/tvix/store/src/tests/fixtures.rs
@@ -0,0 +1,175 @@
+use crate::{
+    proto::{self, Directory, DirectoryNode, FileNode, SymlinkNode},
+    B3Digest,
+};
+use lazy_static::lazy_static;
+
+pub const HELLOWORLD_BLOB_CONTENTS: &[u8] = b"Hello World!";
+pub const EMPTY_BLOB_CONTENTS: &[u8] = b"";
+
+lazy_static! {
+    pub static ref DUMMY_DIGEST: Vec<u8> = vec![
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00,
+    ];
+    pub static ref DUMMY_DATA_1: Vec<u8> = vec![0x01, 0x02, 0x03];
+    pub static ref DUMMY_DATA_2: Vec<u8> = vec![0x04, 0x05];
+
+    pub static ref HELLOWORLD_BLOB_DIGEST: B3Digest =
+        blake3::hash(HELLOWORLD_BLOB_CONTENTS).as_bytes().into();
+    pub static ref EMPTY_BLOB_DIGEST: B3Digest =
+        blake3::hash(EMPTY_BLOB_CONTENTS).as_bytes().into();
+
+    // 2 bytes
+    pub static ref BLOB_A: Vec<u8> = vec![0x00, 0x01];
+    pub static ref BLOB_A_DIGEST: B3Digest = blake3::hash(&BLOB_A).as_bytes().into();
+
+    // 1MB
+    pub static ref BLOB_B: Vec<u8> = (0..255).collect::<Vec<u8>>().repeat(4 * 1024);
+    pub static ref BLOB_B_DIGEST: B3Digest = blake3::hash(&BLOB_B).as_bytes().into();
+
+    // Directories
+    pub static ref DIRECTORY_WITH_KEEP: proto::Directory = proto::Directory {
+        directories: vec![],
+        files: vec![FileNode {
+            name: ".keep".to_string(),
+            digest: EMPTY_BLOB_DIGEST.to_vec(),
+            size: 0,
+            executable: false,
+        }],
+        symlinks: vec![],
+    };
+    pub static ref DIRECTORY_COMPLICATED: proto::Directory = proto::Directory {
+        directories: vec![DirectoryNode {
+            name: "keep".to_string(),
+            digest: DIRECTORY_WITH_KEEP.digest().to_vec(),
+            size: DIRECTORY_WITH_KEEP.size(),
+        }],
+        files: vec![FileNode {
+            name: ".keep".to_string(),
+            digest: EMPTY_BLOB_DIGEST.to_vec(),
+            size: 0,
+            executable: false,
+        }],
+        symlinks: vec![SymlinkNode {
+            name: "aa".to_string(),
+            target: "/nix/store/somewhereelse".to_string(),
+        }],
+    };
+    pub static ref DIRECTORY_A: Directory = Directory::default();
+    pub static ref DIRECTORY_B: Directory = Directory {
+        directories: vec![DirectoryNode {
+            name: "a".to_string(),
+            digest: DIRECTORY_A.digest().to_vec(),
+            size: DIRECTORY_A.size(),
+        }],
+        ..Default::default()
+    };
+    pub static ref DIRECTORY_C: Directory = Directory {
+        directories: vec![
+            DirectoryNode {
+                name: "a".to_string(),
+                digest: DIRECTORY_A.digest().to_vec(),
+                size: DIRECTORY_A.size(),
+            },
+            DirectoryNode {
+                name: "a'".to_string(),
+                digest: DIRECTORY_A.digest().to_vec(),
+                size: DIRECTORY_A.size(),
+            }
+        ],
+        ..Default::default()
+    };
+
+    // output hash
+    pub static ref DUMMY_OUTPUT_HASH: Vec<u8> = vec![
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00
+    ];
+
+    /// The NAR representation of a symlink pointing to `/nix/store/somewhereelse`
+    pub static ref NAR_CONTENTS_SYMLINK: Vec<u8> = vec![
+        13, 0, 0, 0, 0, 0, 0, 0, b'n', b'i', b'x', b'-', b'a', b'r', b'c', b'h', b'i', b'v', b'e', b'-', b'1', 0,
+        0, 0, // "nix-archive-1"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b't', b'y', b'p', b'e', 0, 0, 0, 0, // "type"
+        7, 0, 0, 0, 0, 0, 0, 0, b's', b'y', b'm', b'l', b'i', b'n', b'k', 0, // "symlink"
+        6, 0, 0, 0, 0, 0, 0, 0, b't', b'a', b'r', b'g', b'e', b't', 0, 0, // target
+        24, 0, 0, 0, 0, 0, 0, 0, b'/', b'n', b'i', b'x', b'/', b's', b't', b'o', b'r', b'e', b'/', b's', b'o',
+        b'm', b'e', b'w', b'h', b'e', b'r', b'e', b'e', b'l', b's',
+        b'e', // "/nix/store/somewhereelse"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0 // ")"
+    ];
+
+    /// The NAR representation of a regular file with the contents "Hello World!"
+    pub static ref NAR_CONTENTS_HELLOWORLD: Vec<u8> = vec![
+        13, 0, 0, 0, 0, 0, 0, 0, b'n', b'i', b'x', b'-', b'a', b'r', b'c', b'h', b'i', b'v', b'e', b'-', b'1', 0,
+        0, 0, // "nix-archive-1"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b't', b'y', b'p', b'e', 0, 0, 0, 0, // "type"
+        7, 0, 0, 0, 0, 0, 0, 0, b'r', b'e', b'g', b'u', b'l', b'a', b'r', 0, // "regular"
+        8, 0, 0, 0, 0, 0, 0, 0, b'c', b'o', b'n', b't', b'e', b'n', b't', b's', // "contents"
+        12, 0, 0, 0, 0, 0, 0, 0, b'H', b'e', b'l', b'l', b'o', b' ', b'W', b'o', b'r', b'l', b'd', b'!', 0, 0,
+        0, 0, // "Hello World!"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0 // ")"
+    ];
+
+    /// The NAR representation of a more complicated directory structure.
+    pub static ref NAR_CONTENTS_COMPLICATED: Vec<u8> = vec![
+        13, 0, 0, 0, 0, 0, 0, 0, b'n', b'i', b'x', b'-', b'a', b'r', b'c', b'h', b'i', b'v', b'e', b'-', b'1', 0,
+        0, 0, // "nix-archive-1"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b't', b'y', b'p', b'e', 0, 0, 0, 0, // "type"
+        9, 0, 0, 0, 0, 0, 0, 0, b'd', b'i', b'r', b'e', b'c', b't', b'o', b'r', b'y', 0, 0, 0, 0, 0, 0, 0, // "directory"
+        5, 0, 0, 0, 0, 0, 0, 0, b'e', b'n', b't', b'r', b'y', 0, 0, 0, // "entry"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b'n', b'a', b'm', b'e', 0, 0, 0, 0, // "name"
+        5, 0, 0, 0, 0, 0, 0, 0, b'.', b'k', b'e', b'e', b'p', 0, 0, 0, // ".keep"
+        4, 0, 0, 0, 0, 0, 0, 0, b'n', b'o', b'd', b'e', 0, 0, 0, 0, // "node"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b't', b'y', b'p', b'e', 0, 0, 0, 0, // "type"
+        7, 0, 0, 0, 0, 0, 0, 0, b'r', b'e', b'g', b'u', b'l', b'a', b'r', 0, // "regular"
+        8, 0, 0, 0, 0, 0, 0, 0, b'c', b'o', b'n', b't', b'e', b'n', b't', b's', // "contents"
+        0, 0, 0, 0, 0, 0, 0, 0, // ""
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        5, 0, 0, 0, 0, 0, 0, 0, b'e', b'n', b't', b'r', b'y', 0, 0, 0, // "entry"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b'n', b'a', b'm', b'e', 0, 0, 0, 0, // "name"
+        2, 0, 0, 0, 0, 0, 0, 0, b'a', b'a', 0, 0, 0, 0, 0, 0, // "aa"
+        4, 0, 0, 0, 0, 0, 0, 0, b'n', b'o', b'd', b'e', 0, 0, 0, 0, // "node"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b't', b'y', b'p', b'e', 0, 0, 0, 0, // "type"
+        7, 0, 0, 0, 0, 0, 0, 0, b's', b'y', b'm', b'l', b'i', b'n', b'k', 0, // "symlink"
+        6, 0, 0, 0, 0, 0, 0, 0, b't', b'a', b'r', b'g', b'e', b't', 0, 0, // target
+        24, 0, 0, 0, 0, 0, 0, 0, b'/', b'n', b'i', b'x', b'/', b's', b't', b'o', b'r', b'e', b'/', b's', b'o',
+        b'm', b'e', b'w', b'h', b'e', b'r', b'e', b'e', b'l', b's',
+        b'e', // "/nix/store/somewhereelse"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        5, 0, 0, 0, 0, 0, 0, 0, b'e', b'n', b't', b'r', b'y', 0, 0, 0, // "entry"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b'n', b'a', b'm', b'e', 0, 0, 0, 0, // "name"
+        4, 0, 0, 0, 0, 0, 0, 0, b'k', b'e', b'e', b'p', 0, 0, 0, 0, // "keep"
+        4, 0, 0, 0, 0, 0, 0, 0, b'n', b'o', b'd', b'e', 0, 0, 0, 0, // "node"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b't', b'y', b'p', b'e', 0, 0, 0, 0, // "type"
+        9, 0, 0, 0, 0, 0, 0, 0, b'd', b'i', b'r', b'e', b'c', b't', b'o', b'r', b'y', 0, 0, 0, 0, 0, 0, 0, // "directory"
+        5, 0, 0, 0, 0, 0, 0, 0, b'e', b'n', b't', b'r', b'y', 0, 0, 0, // "entry"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b'n', b'a', b'm', b'e', 0, 0, 0, 0, // "name"
+        5, 0, 0, 0, 0, 0, 0, 0, 46, 107, 101, 101, 112, 0, 0, 0, // ".keep"
+        4, 0, 0, 0, 0, 0, 0, 0, 110, 111, 100, 101, 0, 0, 0, 0, // "node"
+        1, 0, 0, 0, 0, 0, 0, 0, b'(', 0, 0, 0, 0, 0, 0, 0, // "("
+        4, 0, 0, 0, 0, 0, 0, 0, b't', b'y', b'p', b'e', 0, 0, 0, 0, // "type"
+        7, 0, 0, 0, 0, 0, 0, 0, b'r', b'e', b'g', b'u', b'l', b'a', b'r', 0, // "regular"
+        8, 0, 0, 0, 0, 0, 0, 0, b'c', b'o', b'n', b't', b'e', b'n', b't', b's', // "contents"
+        0, 0, 0, 0, 0, 0, 0, 0, // ""
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+        1, 0, 0, 0, 0, 0, 0, 0, b')', 0, 0, 0, 0, 0, 0, 0, // ")"
+    ];
+}
diff --git a/tvix/store/src/tests/import.rs b/tvix/store/src/tests/import.rs
new file mode 100644
index 000000000000..8b66cb024bf0
--- /dev/null
+++ b/tvix/store/src/tests/import.rs
@@ -0,0 +1,113 @@
+use super::utils::{gen_blob_service, gen_directory_service};
+use crate::blobservice::BlobService;
+use crate::directoryservice::DirectoryService;
+use crate::import::ingest_path;
+use crate::proto;
+use crate::tests::fixtures::DIRECTORY_COMPLICATED;
+use crate::tests::fixtures::*;
+use tempfile::TempDir;
+
+#[cfg(target_family = "unix")]
+#[test]
+fn symlink() {
+    let tmpdir = TempDir::new().unwrap();
+
+    std::fs::create_dir_all(&tmpdir).unwrap();
+    std::os::unix::fs::symlink(
+        "/nix/store/somewhereelse",
+        tmpdir.path().join("doesntmatter"),
+    )
+    .unwrap();
+
+    let root_node = ingest_path(
+        &mut gen_blob_service(),
+        &mut gen_directory_service(),
+        tmpdir.path().join("doesntmatter"),
+    )
+    .expect("must succeed");
+
+    assert_eq!(
+        crate::proto::node::Node::Symlink(proto::SymlinkNode {
+            name: "doesntmatter".to_string(),
+            target: "/nix/store/somewhereelse".to_string(),
+        }),
+        root_node,
+    )
+}
+
+#[test]
+fn single_file() {
+    let tmpdir = TempDir::new().unwrap();
+
+    std::fs::write(tmpdir.path().join("root"), HELLOWORLD_BLOB_CONTENTS).unwrap();
+
+    let mut blob_service = gen_blob_service();
+
+    let root_node = ingest_path(
+        &mut blob_service,
+        &mut gen_directory_service(),
+        tmpdir.path().join("root"),
+    )
+    .expect("must succeed");
+
+    assert_eq!(
+        crate::proto::node::Node::File(proto::FileNode {
+            name: "root".to_string(),
+            digest: HELLOWORLD_BLOB_DIGEST.to_vec(),
+            size: HELLOWORLD_BLOB_CONTENTS.len() as u32,
+            executable: false,
+        }),
+        root_node,
+    );
+
+    // ensure the blob has been uploaded
+    assert!(blob_service.has(&HELLOWORLD_BLOB_DIGEST).unwrap());
+}
+
+#[test]
+fn complicated() {
+    let tmpdir = TempDir::new().unwrap();
+
+    // File ``.keep`
+    std::fs::write(tmpdir.path().join(".keep"), vec![]).unwrap();
+    // Symlink `aa`
+    std::os::unix::fs::symlink("/nix/store/somewhereelse", tmpdir.path().join("aa")).unwrap();
+    // Directory `keep`
+    std::fs::create_dir(tmpdir.path().join("keep")).unwrap();
+    // File ``keep/.keep`
+    std::fs::write(tmpdir.path().join("keep").join(".keep"), vec![]).unwrap();
+
+    let mut blob_service = gen_blob_service();
+    let mut directory_service = gen_directory_service();
+
+    let root_node = ingest_path(&mut blob_service, &mut directory_service, tmpdir.path())
+        .expect("must succeed");
+
+    // ensure root_node matched expectations
+    assert_eq!(
+        crate::proto::node::Node::Directory(proto::DirectoryNode {
+            name: tmpdir
+                .path()
+                .file_name()
+                .unwrap()
+                .to_string_lossy()
+                .to_string(),
+            digest: DIRECTORY_COMPLICATED.digest().to_vec(),
+            size: DIRECTORY_COMPLICATED.size(),
+        }),
+        root_node,
+    );
+
+    // ensure DIRECTORY_WITH_KEEP and DIRECTORY_COMPLICATED have been uploaded
+    assert!(directory_service
+        .get(&DIRECTORY_WITH_KEEP.digest())
+        .unwrap()
+        .is_some());
+    assert!(directory_service
+        .get(&DIRECTORY_COMPLICATED.digest())
+        .unwrap()
+        .is_some());
+
+    // ensure EMPTY_BLOB_CONTENTS has been uploaded
+    assert!(blob_service.has(&EMPTY_BLOB_DIGEST).unwrap());
+}
diff --git a/tvix/store/src/tests/mod.rs b/tvix/store/src/tests/mod.rs
new file mode 100644
index 000000000000..8ceea01e3190
--- /dev/null
+++ b/tvix/store/src/tests/mod.rs
@@ -0,0 +1,4 @@
+pub mod fixtures;
+mod import;
+mod nar_renderer;
+pub mod utils;
diff --git a/tvix/store/src/tests/nar_renderer.rs b/tvix/store/src/tests/nar_renderer.rs
new file mode 100644
index 000000000000..f13107b1e48d
--- /dev/null
+++ b/tvix/store/src/tests/nar_renderer.rs
@@ -0,0 +1,189 @@
+use crate::blobservice::BlobService;
+use crate::blobservice::BlobWriter;
+use crate::directoryservice::DirectoryService;
+use crate::nar::NARRenderer;
+use crate::proto::DirectoryNode;
+use crate::proto::FileNode;
+use crate::proto::SymlinkNode;
+use crate::tests::fixtures::*;
+use crate::tests::utils::*;
+use std::io;
+
+#[test]
+fn single_symlink() {
+    let renderer = NARRenderer::new(gen_blob_service(), gen_directory_service());
+    // don't put anything in the stores, as we don't actually do any requests.
+
+    let mut buf: Vec<u8> = vec![];
+
+    renderer
+        .write_nar(
+            &mut buf,
+            &crate::proto::node::Node::Symlink(SymlinkNode {
+                name: "doesntmatter".to_string(),
+                target: "/nix/store/somewhereelse".to_string(),
+            }),
+        )
+        .expect("must succeed");
+
+    assert_eq!(buf, NAR_CONTENTS_SYMLINK.to_vec());
+}
+
+/// Make sure the NARRenderer fails if the blob size in the proto node doesn't
+/// match what's in the store.
+#[test]
+fn single_file_missing_blob() {
+    let renderer = NARRenderer::new(gen_blob_service(), gen_directory_service());
+    let mut buf: Vec<u8> = vec![];
+
+    let e = renderer
+        .write_nar(
+            &mut buf,
+            &crate::proto::node::Node::File(FileNode {
+                name: "doesntmatter".to_string(),
+                digest: HELLOWORLD_BLOB_DIGEST.to_vec(),
+                size: HELLOWORLD_BLOB_CONTENTS.len() as u32,
+                executable: false,
+            }),
+        )
+        .expect_err("must fail");
+
+    match e {
+        crate::nar::RenderError::NARWriterError(e) => {
+            assert_eq!(io::ErrorKind::NotFound, e.kind());
+        }
+        _ => panic!("unexpected error: {:?}", e),
+    }
+}
+
+/// Make sure the NAR Renderer fails if the returned blob meta has another size
+/// than specified in the proto node.
+#[test]
+fn single_file_wrong_blob_size() {
+    let blob_service = gen_blob_service();
+
+    // insert blob into the store
+    let mut writer = blob_service.open_write().unwrap();
+    io::copy(
+        &mut io::Cursor::new(HELLOWORLD_BLOB_CONTENTS.to_vec()),
+        &mut writer,
+    )
+    .unwrap();
+    assert_eq!(HELLOWORLD_BLOB_DIGEST.clone(), writer.close().unwrap());
+
+    let renderer = NARRenderer::new(blob_service, gen_directory_service());
+
+    // Test with a root FileNode of a too big size
+    {
+        let mut buf: Vec<u8> = vec![];
+        let e = renderer
+            .write_nar(
+                &mut buf,
+                &crate::proto::node::Node::File(FileNode {
+                    name: "doesntmatter".to_string(),
+                    digest: HELLOWORLD_BLOB_DIGEST.to_vec(),
+                    size: 42, // <- note the wrong size here!
+                    executable: false,
+                }),
+            )
+            .expect_err("must fail");
+
+        match e {
+            crate::nar::RenderError::NARWriterError(e) => {
+                assert_eq!(io::ErrorKind::UnexpectedEof, e.kind());
+            }
+            _ => panic!("unexpected error: {:?}", e),
+        }
+    }
+
+    // Test with a root FileNode of a too small size
+    {
+        let mut buf: Vec<u8> = vec![];
+        let e = renderer
+            .write_nar(
+                &mut buf,
+                &crate::proto::node::Node::File(FileNode {
+                    name: "doesntmatter".to_string(),
+                    digest: HELLOWORLD_BLOB_DIGEST.to_vec(),
+                    size: 2, // <- note the wrong size here!
+                    executable: false,
+                }),
+            )
+            .expect_err("must fail");
+
+        match e {
+            crate::nar::RenderError::NARWriterError(e) => {
+                assert_eq!(io::ErrorKind::InvalidInput, e.kind());
+            }
+            _ => panic!("unexpected error: {:?}", e),
+        }
+    }
+}
+
+#[test]
+fn single_file() {
+    let blob_service = gen_blob_service();
+
+    // insert blob into the store
+    let mut writer = blob_service.open_write().unwrap();
+    io::copy(
+        &mut io::Cursor::new(HELLOWORLD_BLOB_CONTENTS.to_vec()),
+        &mut writer,
+    )
+    .unwrap();
+    assert_eq!(HELLOWORLD_BLOB_DIGEST.clone(), writer.close().unwrap());
+
+    let renderer = NARRenderer::new(blob_service, gen_directory_service());
+    let mut buf: Vec<u8> = vec![];
+
+    renderer
+        .write_nar(
+            &mut buf,
+            &crate::proto::node::Node::File(FileNode {
+                name: "doesntmatter".to_string(),
+                digest: HELLOWORLD_BLOB_DIGEST.to_vec(),
+                size: HELLOWORLD_BLOB_CONTENTS.len() as u32,
+                executable: false,
+            }),
+        )
+        .expect("must succeed");
+
+    assert_eq!(buf, NAR_CONTENTS_HELLOWORLD.to_vec());
+}
+
+#[test]
+fn test_complicated() {
+    let blob_service = gen_blob_service();
+    let directory_service = gen_directory_service();
+
+    // put all data into the stores.
+    // insert blob into the store
+    let mut writer = blob_service.open_write().unwrap();
+    io::copy(
+        &mut io::Cursor::new(EMPTY_BLOB_CONTENTS.to_vec()),
+        &mut writer,
+    )
+    .unwrap();
+    assert_eq!(EMPTY_BLOB_DIGEST.clone(), writer.close().unwrap());
+
+    directory_service.put(DIRECTORY_WITH_KEEP.clone()).unwrap();
+    directory_service
+        .put(DIRECTORY_COMPLICATED.clone())
+        .unwrap();
+
+    let renderer = NARRenderer::new(blob_service, directory_service);
+    let mut buf: Vec<u8> = vec![];
+
+    renderer
+        .write_nar(
+            &mut buf,
+            &crate::proto::node::Node::Directory(DirectoryNode {
+                name: "doesntmatter".to_string(),
+                digest: DIRECTORY_COMPLICATED.digest().to_vec(),
+                size: DIRECTORY_COMPLICATED.size(),
+            }),
+        )
+        .expect("must succeed");
+
+    assert_eq!(buf, NAR_CONTENTS_COMPLICATED.to_vec());
+}
diff --git a/tvix/store/src/tests/utils.rs b/tvix/store/src/tests/utils.rs
new file mode 100644
index 000000000000..2991feed41db
--- /dev/null
+++ b/tvix/store/src/tests/utils.rs
@@ -0,0 +1,17 @@
+use crate::{
+    blobservice::{BlobService, MemoryBlobService},
+    directoryservice::{DirectoryService, MemoryDirectoryService},
+    pathinfoservice::{MemoryPathInfoService, PathInfoService},
+};
+
+pub fn gen_blob_service() -> impl BlobService + Send + Sync + Clone + 'static {
+    MemoryBlobService::default()
+}
+
+pub fn gen_directory_service() -> impl DirectoryService + Send + Sync + Clone + 'static {
+    MemoryDirectoryService::default()
+}
+
+pub fn gen_pathinfo_service() -> impl PathInfoService {
+    MemoryPathInfoService::default()
+}
diff --git a/tvix/verify-lang-tests/default.nix b/tvix/verify-lang-tests/default.nix
new file mode 100644
index 000000000000..2267ecb77d2e
--- /dev/null
+++ b/tvix/verify-lang-tests/default.nix
@@ -0,0 +1,197 @@
+# SPDX-License-Identifier: LGPL-2.1-only
+# SPDX-FileCopyrightText: © 2022 The TVL Contributors
+# SPDX-FileCopyrightText: © 2004-2022 The Nix Contributors
+#
+# Execute language tests found in tvix_tests and nix_tests
+# using the C++ Nix implementation. Based on NixOS/nix:tests/lang.sh.
+{ depot, pkgs, lib, ... }:
+
+let
+  testRoot = ../eval/src/tests;
+
+  inherit (pkgs.buildPackages) nix nix_latest;
+
+  parseTest = dir: baseName:
+    let
+      tokens = builtins.match "(eval|parse)-(okay|fail).+\\.nix" baseName;
+    in
+    if tokens == null
+    then null
+    else {
+      type = builtins.elemAt tokens 0;
+      expectedSuccess = (builtins.elemAt tokens 1) == "okay";
+      fileName = "${dir}/${baseName}";
+    };
+
+  allLangTests =
+    lib.concatMap
+      (
+        dir:
+        lib.pipe
+          (builtins.readDir (testRoot + "/${dir}"))
+          [
+            builtins.attrNames
+            (builtins.map (parseTest dir))
+            (builtins.filter (t: t != null))
+          ]
+      ) [ "nix_tests" "nix_tests/notyetpassing" "tvix_tests" "tvix_tests/notyetpassing" ];
+
+  skippedLangTests = {
+    # TODO(sterni): set up NIX_PATH in sandbox
+    "eval-okay-search-path.nix" = true;
+    # Floating point precision differs between tvix and Nix
+    "eval-okay-fromjson.nix" = true;
+    # C++ Nix can't TCO
+    "eval-okay-tail-call-1.nix" = true;
+    # Ordering change after 2.3
+    "eval-okay-xml.nix" = [ nix ];
+    # Missing builtins in Nix 2.3
+    "eval-okay-ceil.nix" = [ nix ];
+    "eval-okay-floor-ceil.nix" = [ nix ];
+    "eval-okay-floor.nix" = [ nix ];
+    "eval-okay-groupBy.nix" = [ nix ];
+    "eval-okay-zipAttrsWith.nix" = [ nix ];
+    # Comparable lists are not in Nix 2.3
+    "eval-okay-sort.nix" = [ nix ];
+    "eval-okay-compare-lists.nix" = [ nix ];
+    "eval-okay-value-pointer-compare.nix" = [ nix ];
+    # getAttrPos gains support for functionArgs-returned sets after 2.3
+    "eval-okay-getattrpos-functionargs.nix" = [ nix ];
+    # groupBy appeared (long) after 2.3
+    "eval-okay-builtins-groupby-thunk.nix" = [ nix ];
+  };
+
+  runCppNixLangTests = cpp-nix:
+    let
+      testCommand = { fileName, type, expectedSuccess, ... }:
+        let
+          testBase = lib.removeSuffix ".nix" fileName;
+          expFile =
+            let
+              possibleFiles =
+                builtins.filter
+                  (path: builtins.pathExists (testRoot + "/${path}"))
+                  (builtins.map
+                    (ext: "${testBase}.${ext}")
+                    [ "exp" "exp.xml" ]);
+            in
+            if possibleFiles == [ ] then null else builtins.head possibleFiles;
+          outFile = "${testBase}.out";
+
+          # Skip if skippedLangTests prescribes it (possibly just for the current nix)
+          # or if we are missing an exp file for an eval-okay test.
+          skip =
+            let
+              doSkip = skippedLangTests.${builtins.baseNameOf fileName} or false;
+            in
+            if type == "eval" && expectedSuccess && (expFile == null) then true
+            else if builtins.isBool doSkip then doSkip
+            else builtins.any (drv: cpp-nix == drv) doSkip;
+
+          flagsFile = "${testBase}.flags";
+
+          instantiateFlags =
+            lib.escapeShellArgs
+              (
+                [ "--${type}" fileName ]
+                ++ lib.optionals (type == "eval") [ "--strict" ]
+                ++ lib.optionals (expFile != null && lib.hasSuffix "xml" expFile)
+                  [
+                    "--no-location"
+                    "--xml"
+                  ]
+              )
+            + lib.optionalString (builtins.pathExists (testRoot + "/${flagsFile}"))
+              " $(cat '${flagsFile}')";
+        in
+
+        if skip
+        then "echo \"SKIP ${type} ${fileName}\"\n"
+        else ''
+          thisTestPassed=true
+
+          echo "RUN  ${type} ${fileName} ${
+            lib.optionalString (!expectedSuccess) "(expecting failure)"
+          }"
+
+          if ! expect ${if expectedSuccess then "0" else "1"} \
+                 nix-instantiate ${instantiateFlags} \
+                 ${if expectedSuccess then "1" else "2"}> \
+                 ${if expFile != null then outFile else "/dev/null"};
+          then
+            echo -n "FAIL"
+            thisTestPassed=false
+          fi
+        '' + lib.optionalString (expFile != null) ''
+          if ! diff --color=always -u '${outFile}' '${expFile}'; then
+            thisTestPassed=false
+          fi
+        '' + ''
+          if $thisTestPassed; then
+            echo -n "PASS"
+          else
+            echo -n "FAIL"
+            passed=false
+          fi
+
+          echo " ${type} ${fileName}"
+
+          unset thisTestPassed
+        '';
+    in
+
+    pkgs.stdenv.mkDerivation {
+      name = "cpp-${cpp-nix.name}-run-lang-tests";
+
+      nativeBuildInputs = [ cpp-nix ];
+
+      # Obtain tests via the unpackPhase
+      src = testRoot;
+      dontConfigure = true;
+
+      # Environment expected by the test suite
+      TEST_VAR = "foo";
+
+      buildPhase = ''
+        # Make nix-instantiate happy in the sandbox
+        export NIX_STORE_DIR="$(realpath "$(mktemp -d store.XXXXXXXXXX)")"
+        export NIX_STATE_DIR="$(realpath "$(mktemp -d state.XXXXXXXXXX)")"
+
+        # Helper function to check expected exit code
+        expect() {
+          local expected res
+          expected="$1"
+          shift
+          set +e
+          "$@"
+          res="$?"
+          set -e
+          [[ $res -eq $expected ]]
+        }
+
+        # Track test results so far
+        passed=true
+
+        source "$testCommandsPath"
+      '';
+
+      # Actually runs into the argv limit
+      passAsFile = [ "testCommands" ];
+      testCommands = lib.concatMapStrings testCommand allLangTests;
+
+      installPhase = ''
+        if $passed; then
+          touch $out
+        else
+          echo "Some test(s) failed!"
+          exit 1
+        fi
+      '';
+    };
+
+in
+
+depot.nix.readTree.drvTargets {
+  "nix-2.3" = runCppNixLangTests nix;
+  "nix-${lib.versions.majorMinor nix_latest.version}" = runCppNixLangTests nix_latest;
+}
diff --git a/tvix/website/default.nix b/tvix/website/default.nix
new file mode 100644
index 000000000000..2dd939e8a939
--- /dev/null
+++ b/tvix/website/default.nix
@@ -0,0 +1,32 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  # https://developers.google.com/search/docs/advanced/structured-data/logo
+  structuredData = {
+    "@context" = "https://schema.org";
+    "@type" = "Organisation";
+    url = "https://tvl.fyi";
+    logo = "https://static.tvl.fyi/latest/logo-animated.svg";
+  };
+
+  # All Tvix-related blog posts from the main TVL website
+  tvixPosts = builtins.filter
+    (post: !(post.draft or false) && (lib.hasInfix "Tvix" post.title))
+    depot.web.tvl.blog.posts;
+
+  postListEntries = map (p: "* [${p.title}](https://tvl.fyi/blog/${p.key})") tvixPosts;
+
+  landing = depot.web.tvl.template {
+    title = "Tvix - A new implementation of Nix";
+    content = ''
+      ${builtins.readFile ./landing-en.md}
+      ${builtins.concatStringsSep "\n" postListEntries}
+    '';
+  };
+
+in
+pkgs.runCommand "tvix-website" { } ''
+  mkdir $out
+  cp ${landing} $out/index.html
+  cp ${./tvix-logo.webp} $out/tvix-logo.webp
+''
diff --git a/tvix/website/landing-en.md b/tvix/website/landing-en.md
new file mode 100644
index 000000000000..d95320f11c21
--- /dev/null
+++ b/tvix/website/landing-en.md
@@ -0,0 +1,36 @@
+<img class="tvl-logo" src="./tvix-logo.webp"
+     alt="A candy bar in different shades of blue that says 'Tvix by TVL' on it">
+
+------------------
+
+Tvix is a new implementation of Nix, a purely-functional package manager. It
+aims to have a modular implementation, in which different components can be
+reused or replaced based on the use-case.
+
+Tvix is developed as a GPLv3-licensed open-source project by
+[TVL][], with source code available in the [TVL monorepo][].
+
+There are several projects within Tvix, such as:
+
+* `//tvix/eval` - an implementation of the Nix programming language
+* `//tvix/serde` - a Rust library for using the Nix language for config files
+* `//tvix/nix-compat` - a Rust library for compatibility with C++ Nix
+  features like encodings and hashing schemes
+* ... and a handful others!
+
+The language evaluator can be toyed with in [Tvixbolt][], and you can check out
+the [Tvix README][] ([GitHub mirror][gh]) for additional information on the
+project and development workflows.
+
+Developer documentation for some parts of Tvix is [available online][docs].
+
+[TVL]: https://tvl.fyi
+[TVL monorepo]: https://cs.tvl.fyi/depot/-/tree/tvix
+[Tvixbolt]: https://tvixbolt.tvl.su
+[Tvix README]: https://code.tvl.fyi/about/tvix
+[gh]: https://github.com/tvlfyi/tvix/
+[docs]: https://docs.tvix.dev
+
+-------------------
+
+Check out the latest Tvix-related blog posts from TVL's website:
diff --git a/tvix/website/tvix-logo.webp b/tvix/website/tvix-logo.webp
new file mode 100644
index 000000000000..07bffc18b7b9
--- /dev/null
+++ b/tvix/website/tvix-logo.webp
Binary files differdiff --git a/users/Profpatsch/.envrc b/users/Profpatsch/.envrc
new file mode 100644
index 000000000000..051d09d292a8
--- /dev/null
+++ b/users/Profpatsch/.envrc
@@ -0,0 +1 @@
+eval "$(lorri direnv)"
diff --git a/users/Profpatsch/.gitignore b/users/Profpatsch/.gitignore
new file mode 100644
index 000000000000..c33954f53a06
--- /dev/null
+++ b/users/Profpatsch/.gitignore
@@ -0,0 +1 @@
+dist-newstyle/
diff --git a/users/Profpatsch/.hlint.yaml b/users/Profpatsch/.hlint.yaml
new file mode 100644
index 000000000000..09e6f2c95401
--- /dev/null
+++ b/users/Profpatsch/.hlint.yaml
@@ -0,0 +1,355 @@
+# HLint configuration file
+# https://github.com/ndmitchell/hlint
+# Run `hlint --default` to see the example configuration file.
+##########################
+
+# WARNING: These need to be synced with the default-extensions field
+# in the cabal file.
+- arguments: [-XGHC2021, -XOverloadedRecordDot]
+
+# Ignore some builtin hints
+
+# often functions are more readable with explicit arguments
+- ignore: { name: Eta reduce }
+
+# these redundancy warnings are just completely irrelevant
+- ignore: { name: Redundant bracket }
+- ignore: { name: Move brackets to avoid $ }
+- ignore: { name: Redundant $ }
+- ignore: { name: Redundant do }
+- ignore: { name: Redundant multi-way if }
+
+# allow case-matching on bool, because why not
+- ignore: { name: Use if }
+
+# hlint cannot distinguish actual newtypes from data types
+# that accidentally have only one field
+# (but might have more in the future).
+# Since it’s a mostly irrelevant runtime optimization, we don’t care.
+- ignore: { name: Use newtype instead of data }
+
+# these lead to harder-to-read/more implicit code
+- ignore: { name: Use fmap }
+- ignore: { name: Use <$> }
+- ignore: { name: Use tuple-section }
+- ignore: { name: Use forM_ }
+# fst and snd are usually a code smell and should be explicit matches, _naming the ignored side.
+- ignore: { name: Use fst }
+- ignore: { name: Use snd }
+- ignore: { name: Use fromMaybe }
+- ignore: { name: Use const }
+- ignore: { name: Replace case with maybe }
+- ignore: { name: Replace case with fromMaybe }
+- ignore: { name: Avoid lambda }
+- ignore: { name: Avoid lambda using `infix` }
+- ignore: { name: Use curry }
+- ignore: { name: Use uncurry }
+- ignore: { name: Use first }
+- ignore: { name: Redundant first }
+- ignore: { name: Use second }
+- ignore: { name: Use bimap }
+# just use `not x`
+- ignore: { name: Use unless }
+- ignore: { name: Redundant <&> }
+
+# list comprehensions are a seldomly used part of the Haskell language
+# and they introduce syntactic overhead that is usually not worth the conciseness
+- ignore: { name: Use list comprehension }
+
+# Seems to be buggy in cases
+- ignore: { name: Use section }
+
+# multiple maps in a row are usually used for clarity,
+# and the compiler will optimize them away, thank you very much.
+- ignore: { name: Use map once }
+- ignore: { name: Fuse foldr/map }
+- ignore: { name: Fuse traverse/map }
+- ignore: { name: Fuse traverse_/map }
+
+# this is silly, why would I use a special function if I can just (heh) `== Nothing`
+- ignore: { name: Use isNothing }
+
+# The duplication heuristic is not very smart
+# and more annoying than helpful.
+# see https://github.com/ndmitchell/hlint/issues/1009
+- ignore: { name: Reduce duplication }
+
+# Stops the pattern match trick
+- ignore: { name: Use record patterns }
+- ignore: { name: Use null }
+- ignore: { name: Use uncurry }
+
+# we don’t want void, see below
+- ignore: { name: Use void }
+
+- functions:
+    # disallow Enum instance functions, they are partial
+    - name: Prelude.succ
+      within: [Relude.Extra.Enum]
+      message: "Dangerous, will fail for highest element"
+    - name: Prelude.pred
+      within: [Relude.Extra.Enum]
+      message: "Dangerous, will fail for lowest element"
+    - name: Prelude.toEnum
+      within: []
+      message: "Extremely partial"
+    - name: Prelude.fromEnum
+      within: []
+      message: "Dangerous for most uses"
+    - name: Prelude.enumFrom
+      within: []
+    - name: Prelude.enumFromThen
+      within: []
+    - name: Prelude.enumFromThenTo
+      within: []
+    - name: Prelude.oundedEnumFrom
+      within: []
+    - name: Prelude.boundedEnumFromThen
+      within: []
+
+    - name: Text.Read.readMaybe
+      within:
+        # The BSON ObjectId depends on Read for parsing
+        - Milkmap.Milkmap
+        - Milkmap.FieldData.Value
+      message: "`readMaybe` is probably not what you want for parsing values, please use the `FieldParser` module."
+
+    # `void` discards its argument and is polymorphic,
+    # thus making it brittle in the face of code changes.
+    # (see https://tech.freckle.com/2020/09/23/void-is-a-smell/)
+    # Use an explicit `_ <- …` instead.
+    - name: Data.Functor.void
+      within: []
+      message: "`void` leads to bugs. Use an explicit `_ <- …` instead"
+
+    - name: Data.Foldable.length
+      within: []
+      message: "`Data.Foldable.length` is dangerous to use, because it also works on types you wouldn’t expect, like `length (3,4) == 1` and `length (Just 2) == 1`. Use the `length` function for your specific type instead, for example `List.length` or `Map.length`."
+
+    - name: Prelude.length
+      within: []
+      message: "`Prelude.length` is dangerous to use, because it also works on types you wouldn’t expect, like `length (3,4) == 1` and `length (Just 2) == 1`. Use the `length` function for your specific type instead, for example `List.length` or `Map.length`."
+
+    # Using an explicit lambda with its argument “underscored”
+    # is more clear in every case.
+    # e.g. `const True` => `\_request -> True`
+    # shows the reader that the ignored argument was a request.
+    - name: Prelude.const
+      within: []
+      message: "Replace `const` with an explicit lambda with type annotation for code clarity and type safety, e.g.: `const True` => `\\(_ :: Request) -> True`. If you really don’t want to spell out the type (which might lead to bugs!), you can also use something like `\_request -> True`."
+
+    - name: Data.List.nub
+      within: []
+      message: "O(n²), use `Data.Containers.ListUtils.nubOrd"
+
+    - name: Prelude.maximum
+      within: []
+      message: "`maximum` crashes on empty list; use non-empty lists and `maximum1`"
+
+    - name: Data.List.maximum
+      within: []
+      message: "`maximum` crashes on empty list; use non-empty lists and `maximum1`"
+
+    - name: Prelude.minimum
+      within: []
+      message: "`minimum` crashes on empty list; use non-empty lists and `minimum1`"
+
+    - name: Data.List.minimum
+      within: []
+      message: "`minimum` crashes on empty list; use non-empty lists and `minimum1`"
+
+    - name: Data.Foldable.maximum
+      within: []
+      message: "`maximum` crashes on empty foldable stucture; use Foldable1 and `maximum1`."
+
+    - name: Data.Foldable.minimum
+      within: []
+      message: "`minimum` crashes on empty foldable stucture; use Foldable1 and `minimum1`."
+
+    # Using prelude functions instead of stdlib functions
+
+    - name: "Data.Text.Encoding.encodeUtf8"
+      within: ["MyPrelude"]
+      message: "Use `textToBytesUtf8`"
+
+    - name: "Data.Text.Lazy.Encoding.encodeUtf8"
+      within: ["MyPrelude"]
+      message: "Use `textToBytesUtf8Lazy`"
+
+    - name: "Data.Text.Encoding.decodeUtf8'"
+      within: ["MyPrelude"]
+      message: "Use `bytesToTextUtf8`"
+
+    - name: "Data.Text.Encoding.Lazy.decodeUtf8'"
+      within: ["MyPrelude"]
+      message: "Use `bytesToTextUtf8Lazy`"
+
+    - name: "Data.Text.Encoding.decodeUtf8"
+      within: ["MyPrelude"]
+      message: "Either check for errors with `bytesToTextUtf8`, decode leniently with unicode replacement characters with `bytesToTextUtf8Lenient` or use the crashing version `bytesToTextUtf8Unsafe` (discouraged)."
+
+    - name: "Data.Text.Encoding.Lazy.decodeUtf8"
+      within: ["MyPrelude"]
+      message: "Either check for errors with `bytesToTextUtf8Lazy`, decode leniently with unicode replacement characters with `bytesToTextUtf8LenientLazy` or use the crashing version `bytesToTextUtf8UnsafeLazy` (discouraged)."
+
+    - name: "Data.Text.Lazy.toStrict"
+      within: ["MyPrelude"]
+      message: "Use `toStrict`"
+
+    - name: "Data.Text.Lazy.fromStrict"
+      within: ["MyPrelude"]
+      message: "Use `toLazy`"
+
+    - name: "Data.ByteString.Lazy.toStrict"
+      within: ["MyPrelude"]
+      message: "Use `toStrictBytes`"
+
+    - name: "Data.ByteString.Lazy.fromStrict"
+      within: ["MyPrelude"]
+      message: "Use `toLazyBytes`"
+
+    - name: "Data.Text.unpack"
+      within: ["MyPrelude"]
+      message: "Use `textToString`"
+
+    - name: "Data.Text.pack"
+      within: ["MyPrelude"]
+      message: "Use `stringToText`"
+
+    - name: "Data.Maybe.listToMaybe"
+      within: []
+      message: |
+        `listToMaybe`` throws away everything but the first element of a list (it is essentially `safeHead`).
+        If that is what you want, please use a pattern match like
+
+        ```
+        case xs of
+          [] -> …
+          (x:_) -> …
+        ```
+
+    - name: "Data.List.head"
+      within: []
+      message: |
+        `List.head` fails on an empty list. I didn’t think I have to say this, but please use a pattern match on the list, like:
+
+        ```
+        case xs of
+          [] -> … error handling …
+          (x:_) -> …
+        ```
+
+        Also think about why the rest of the list should be ignored.
+
+    - name: "Prelude.head"
+      within: []
+      message: |
+        `List.head` fails on an empty list. I didn’t think I have to say this, but please use a pattern match on the list, like.
+
+        ```
+        case xs of
+          [] -> … error handling …
+          (x:_) -> …
+        ```
+
+        Also think about why the rest of the list should be ignored.
+
+    - name: "Data.Maybe.fromJust"
+      within: []
+      message: |
+        `Maybe.fromJust` is obviously partial. Please use a pattern match.
+
+        In case you actually want to throw an error on an empty list,
+        please add an error message, like so:
+
+        ```
+        myMaybe & annotate "my error message" & unwrapError
+        ```
+
+        If you are in `IO`, use `unwrapIOError` instead,
+        or throw a monad-specific error.
+
+    - name: "Data.Either.fromLeft"
+      within: []
+      message: |
+        `Either.fromLeft` is obviously partial. Please use a pattern match.
+
+    - name: "Data.Either.fromRight"
+      within: []
+      message: |
+        `Either.fromRight` is obviously partial. Please use a pattern match.
+
+# Make restricted functions into an error if found
+- error: { name: "Avoid restricted function, see comment in .hlint.yaml" }
+
+# Some functions that have (more modern) aliases.
+# They are not dangerous per se,
+# but we want to make it easier to read our code so we should
+# make sure we don’t use too many things that are renames.
+
+- hint:
+    lhs: "undefined"
+    rhs: "todo"
+    note: "`undefined` is a silent error, `todo` will display a warning as long as it exists in the code."
+
+- hint:
+    lhs: "return"
+    rhs: "pure"
+    note: "Use `pure` from `Applicative` instead, it’s the exact same function."
+
+- hint:
+    lhs: "mapM"
+    rhs: "traverse"
+    note: "Use `traverse` from `Traversable` instead. It’s the exact same function."
+
+- hint:
+    lhs: "mapM_"
+    rhs: "traverse_"
+    note: "Use `traverse_` from `Traversable` instead. It’s the exact same function."
+
+- hint:
+    lhs: "forM"
+    rhs: "for"
+    note: "Use `for` from `Traversable` instead. It’s the exact same function."
+
+- hint:
+    lhs: "forM_"
+    rhs: "for_"
+    note: "Use `for_` from `Traversable` instead. It’s the exact same function."
+
+- hint:
+    lhs: "stringToText (show x)"
+    rhs: "showToText x"
+
+- hint:
+    lhs: "Data.Set.toList (Data.Set.fromList x)"
+    rhs: "List.nubOrd x"
+    note: "`nubOrd` removes duplicate elements from a list."
+
+- modules:
+    # Disallowed Modules
+    - name: Data.Map
+      within: []
+      message: "Lazy maps leak space, use `import Data.Map.Strict as Map` instead"
+    - name: Control.Monad.Writer
+      within: []
+      message: "Lazy writers leak space, use `Control.Monad.Trans.Writer.CPS` instead"
+    - name: Control.Monad.Trans.Writer.Lazy
+      within: []
+      message: "Lazy writers leak space, use `Control.Monad.Trans.Writer.CPS` instead"
+    - name: Control.Monad.Trans.Writer.Strict
+      within: []
+      message: "Even strict writers leak space, use `Control.Monad.Trans.Writer.CPS` instead"
+
+    # Qualified module imports
+    - { name: Data.Map.Strict, as: Map }
+    - { name: Data.HashMap.Strict, as: HashMap }
+    - { name: Data.Set, as: Set }
+    - { name: Data.ByteString.Char8, as: Char8 }
+    - { name: Data.ByteString.Lazy.Char8, as: Char8.Lazy }
+    - { name: Data.Text, as: Text }
+    - { name: Data.Vector, as: Vector }
+    - { name: Data.Vault.Lazy, as: Vault }
+    - { name: Data.Aeson, as: Json }
+    - { name: Data.Aeson.Types, as: Json }
+    - { name: Data.Aeson.BetterErrors as Json }
diff --git a/users/Profpatsch/.vscode/settings.json b/users/Profpatsch/.vscode/settings.json
new file mode 100644
index 000000000000..81546964eb03
--- /dev/null
+++ b/users/Profpatsch/.vscode/settings.json
@@ -0,0 +1,14 @@
+{
+    "sqltools.connections": [
+        {
+            "previewLimit": 50,
+            "driver": "SQLite",
+            "name": "cas-serve",
+            "database": "${workspaceFolder:Profpatsch}/cas-serve/data.sqlite"
+        }
+    ],
+    "sqltools.useNodeRuntime": true,
+    "[haskell]": {
+        "editor.formatOnSave": true
+    }
+}
diff --git a/users/Profpatsch/OWNERS b/users/Profpatsch/OWNERS
new file mode 100644
index 000000000000..ac23e72256b1
--- /dev/null
+++ b/users/Profpatsch/OWNERS
@@ -0,0 +1,4 @@
+set noparent
+
+Profpatsch
+sterni
diff --git a/users/Profpatsch/README.md b/users/Profpatsch/README.md
new file mode 100644
index 000000000000..5bb74cd7580f
--- /dev/null
+++ b/users/Profpatsch/README.md
@@ -0,0 +1,10 @@
+# Profpatsch’s assemblage of peculiarities and curiosities
+
+Welcome, Welcome.
+
+Welcome to my user dir, where we optimize f*** around, in order to optimize finding out.
+
+![fafo graph](./fafo.jpg)
+
+DISCLAIMER: All of this code is of the “do not try at work” sort, unless noted otherwise.
+You might try at home, however. Get inspired or get grossed out, whichever you like.
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 000000000000..e636017a54d5
--- /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 000000000000..e3b27c382a21
--- /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 000000000000..4d6baf946c3e
--- /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 000000000000..36bbed7146d6
--- /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/alacritty.nix b/users/Profpatsch/alacritty.nix
new file mode 100644
index 000000000000..917db0541624
--- /dev/null
+++ b/users/Profpatsch/alacritty.nix
@@ -0,0 +1,31 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.alacritty [ "alacritty" ];
+
+  config =
+    {
+      alacritty-config = { font.size = 18; scolling.history = 1000000; };
+      #  This disables the dpi-sensitive scaling (cause otherwise the font will be humongous on my laptop screen)
+      alacritty-env.WINIT_X11_SCALE_FACTOR = 1;
+    };
+
+
+  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 000000000000..6a1c2c1a63c3
--- /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/ArglibNetencode.hs b/users/Profpatsch/arglib/ArglibNetencode.hs
new file mode 100644
index 000000000000..e4609d26abf5
--- /dev/null
+++ b/users/Profpatsch/arglib/ArglibNetencode.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module ArglibNetencode where
+
+import qualified Data.Attoparsec.ByteString as Atto
+import Data.Maybe (fromMaybe)
+import ExecHelpers
+import MyPrelude
+import Netencode
+import qualified System.Environment as Env
+import qualified System.Posix.Env.ByteString as ByteEnv
+
+arglibNetencode :: CurrentProgramName -> Maybe Text -> IO T
+arglibNetencode progName mEnvvar = do
+  let envvar = mEnvvar & fromMaybe "ARGLIB_NETENCODE" & textToBytesUtf8
+  ByteEnv.getEnv envvar >>= \case
+    Nothing -> dieUserError progName [fmt|could not read args, envvar {envvar} not set|]
+    Just bytes ->
+      case Atto.parseOnly (Netencode.netencodeParser <* Atto.endOfInput) bytes of
+        Left err -> dieEnvironmentProblem progName [fmt|arglib parsing error: {err}|]
+        Right t -> do
+          ByteEnv.unsetEnv envvar
+          pure t
diff --git a/users/Profpatsch/arglib/arglib-netencode.cabal b/users/Profpatsch/arglib/arglib-netencode.cabal
new file mode 100644
index 000000000000..d9a049247395
--- /dev/null
+++ b/users/Profpatsch/arglib/arglib-netencode.cabal
@@ -0,0 +1,17 @@
+cabal-version:      2.4
+name:               arglib-netencode
+version:            0.1.0.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+library
+    exposed-modules:          ArglibNetencode
+
+    build-depends:
+        base >=4.15 && <5,
+        my-prelude,
+        netencode,
+        exec-helpers,
+        attoparsec,
+        unix
+    default-language: Haskell2010
diff --git a/users/Profpatsch/arglib/netencode.nix b/users/Profpatsch/arglib/netencode.nix
new file mode 100644
index 000000000000..88bad97a3a7e
--- /dev/null
+++ b/users/Profpatsch/arglib/netencode.nix
@@ -0,0 +1,79 @@
+{ depot, pkgs, lib, ... }:
+
+let
+
+  # Add the given nix arguments to the program as ARGLIB_NETENCODE envvar
+  #
+  # Calls `netencode.gen.dwim` on the provided nix args value.
+  with-args = name: args: prog: depot.nix.writeExecline "${name}-with-args" { } [
+    "export"
+    "ARGLIB_NETENCODE"
+    (depot.users.Profpatsch.netencode.gen.dwim args)
+    prog
+  ];
+
+  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
+    }
+  '';
+
+  haskell = pkgs.haskellPackages.mkDerivation {
+    pname = "arglib-netencode";
+    version = "0.1.0";
+
+    src = depot.users.Profpatsch.exactSource ./. [
+      ./arglib-netencode.cabal
+      ./ArglibNetencode.hs
+    ];
+
+    libraryHaskellDepends = [
+      depot.users.Profpatsch.my-prelude
+      depot.users.Profpatsch.netencode.netencode-hs
+      depot.users.Profpatsch.execline.exec-helpers-hs
+    ];
+
+    isLibrary = true;
+    license = lib.licenses.mit;
+
+
+  };
+
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    with-args
+    rust
+    haskell
+    ;
+}
diff --git a/users/Profpatsch/atomically-write.nix b/users/Profpatsch/atomically-write.nix
new file mode 100644
index 000000000000..c4d07cfbb1fa
--- /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/README.md b/users/Profpatsch/blog/README.md
new file mode 100644
index 000000000000..0753ebdea542
--- /dev/null
+++ b/users/Profpatsch/blog/README.md
@@ -0,0 +1,7 @@
+# (Parts of) my website
+
+This is a part of https://profpatsch.de/, notably the blog posts.
+
+The other parts can be found in [vuizvui](https://github.com/openlab-aux/vuizvui/tree/master/pkgs/profpatsch/profpatsch.de). It’s a mess.
+
+And yes, this implements a webserver & routing engine with nix, execline & s6 utils. “Bis einer weint”, as we say in German.
diff --git a/users/Profpatsch/blog/default.nix b/users/Profpatsch/blog/default.nix
new file mode 100644
index 000000000000..9f7b0fdfa255
--- /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 000000000000..5c6b39f6e81b
--- /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 000000000000..59ea4f747700
--- /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 000000000000..99071ef9d370
--- /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 000000000000..ba80888badd8
--- /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/cas-serve/CasServe.hs b/users/Profpatsch/cas-serve/CasServe.hs
new file mode 100644
index 000000000000..f7189d5f9acf
--- /dev/null
+++ b/users/Profpatsch/cas-serve/CasServe.hs
@@ -0,0 +1,239 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeApplications #-}
+{-# OPTIONS_GHC -Wall #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Main where
+
+import Control.Applicative
+import Control.Monad.Reader
+import Crypto.Hash qualified as Crypto
+import Data.ByteArray qualified as ByteArray
+import Data.ByteString.Lazy qualified as ByteString.Lazy
+import Data.ByteString.Lazy qualified as Lazy
+import Data.Functor.Compose
+import Data.Int (Int64)
+import Data.List qualified as List
+import Data.Maybe (fromMaybe)
+import Data.Text qualified as Text
+import Data.Text.IO qualified as Text
+import Database.SQLite.Simple (NamedParam ((:=)))
+import Database.SQLite.Simple qualified as Sqlite
+import Database.SQLite.Simple.FromField qualified as Sqlite
+import Database.SQLite.Simple.QQ qualified as Sqlite
+import Label
+import MyPrelude
+import Network.HTTP.Types qualified as Http
+import Network.Wai qualified as Wai
+import Network.Wai.Handler.Warp qualified as Warp
+import System.IO (stderr)
+
+main :: IO ()
+main = do
+  withEnv $ \env ->
+    Warp.runSettings
+      (Warp.defaultSettings & Warp.setPort 7070)
+      (api env)
+
+withEnv :: (Env -> IO a) -> IO a
+withEnv inner = do
+  withSqlite "./data.sqlite" $ \envData -> do
+    withSqlite "./wordlist.sqlite" $ \envWordlist -> inner Env {..}
+
+withSqlite :: String -> (Sqlite.Connection -> IO a) -> IO a
+withSqlite fileName inner = Sqlite.withConnection fileName $ \conn -> do
+  Sqlite.setTrace conn (Just (\msg -> Text.hPutStrLn stderr [fmt|{fileName}: {msg}|]))
+  Sqlite.execute conn [Sqlite.sql|PRAGMA foreign_keys = ON|] ()
+  inner conn
+
+api :: Env -> Wai.Application
+api env req respond = do
+  case runHandler (getById <|> insertById) req env of
+    Nothing -> respond $ Wai.responseLBS Http.status404 [] "endpoint does not exist."
+    Just handler' -> do
+      handler' >>= \case
+        Left (status, err) -> respond $ Wai.responseLBS status [] (err & toLazyBytes)
+        Right (headers, body) ->
+          respond $
+            Wai.responseLBS
+              Http.status200
+              headers
+              ( body & toLazyBytes
+              )
+
+data Env = Env
+  { envWordlist :: Sqlite.Connection,
+    envData :: Sqlite.Connection
+  }
+
+-- | I don’t need any fancy routing in this, so a handler is just something that returns a @Just (IO a)@ if it wants to handle the request.
+newtype Handler a
+  = Handler (ReaderT (Wai.Request, Env) (Compose Maybe IO) a)
+  deriving newtype (Functor, Applicative, Alternative)
+
+handler :: ((Wai.Request, Env) -> Maybe (IO a)) -> Handler a
+handler f = Handler (ReaderT (Compose . f))
+
+runHandler :: Handler a -> Wai.Request -> Env -> Maybe (IO a)
+runHandler (Handler handler') req env = getCompose $ handler' & (\readerT -> runReaderT readerT (req, env))
+
+getById ::
+  Handler
+    ( Either
+        (Http.Status, ByteString)
+        ([(Http.HeaderName, ByteString)], ByteString)
+    )
+getById = handler $ \(req, env) -> do
+  guard ((req & Wai.requestMethod) == Http.methodGet)
+  case req & Wai.pathInfo of
+    ["v0", "by-id", filename] -> Just $ do
+      Sqlite.queryNamed
+        @( T3
+             "mimetype"
+             Text
+             "content"
+             ByteString
+             "size"
+             Int
+         )
+        (env & envData)
+        [Sqlite.sql|
+        SELECT
+          mimetype,
+          cast (content AS blob) as content,
+          size
+        FROM file_content
+        JOIN file_references
+          ON file_references.file_content = file_content.hash_sha256
+        WHERE
+          file_references.reference_type = 'by-id'
+          AND (file_references.name || file_references.extension) = :filename
+       |]
+        [":filename" Sqlite.:= filename]
+        <&> \case
+          [] -> Left (Http.status404, "File not found.")
+          [res] ->
+            Right
+              ( [ ("Content-Type", res.mimetype & textToBytesUtf8),
+                  ("Content-Length", res.size & showToText & textToBytesUtf8)
+                ],
+                -- TODO: should this be lazy/streamed?
+                res.content
+              )
+          _more -> Left "file_references must be unique (in type and name)" & unwrapError
+    _ -> Nothing
+
+insertById :: Handler (Either a ([(Http.HeaderName, ByteString)], ByteString))
+insertById = handler $ \(req, env) -> do
+  guard ((req & Wai.requestMethod) == Http.methodPost)
+  case req & Wai.pathInfo of
+    ["v0", "by-id"] -> Just $ do
+      let maybeText bytes = case bytesToTextUtf8 bytes of
+            Left _err -> Nothing
+            Right t -> Just t
+      let mimeType =
+            ( (req & Wai.requestHeaders & List.lookup "X-Cas-Serve-Mimetype" >>= maybeText)
+                <|> (req & Wai.requestHeaders & List.lookup "Content-Type" >>= maybeText)
+            )
+              & fromMaybe "application/octet-stream"
+
+      let magicFileEnding mimeType' = case Text.split (== '/') mimeType' of
+            [_, ""] -> Nothing
+            ["", _] -> Nothing
+            [_, "any"] -> Nothing
+            ["image", ty] -> Just (Text.cons '.' ty)
+            ["video", ty] -> Just (Text.cons '.' ty)
+            ["text", "plain"] -> Just ".txt"
+            ["text", "html"] -> Just ".html"
+            ["application", "pdf"] -> Just ".pdf"
+            ["application", "json"] -> Just ".json"
+            _ -> Nothing
+
+      let extension =
+            ( (req & Wai.requestHeaders & List.lookup "X-Cas-Serve-FileExtension" >>= maybeText)
+                <|> ( (req & Wai.requestHeaders & List.lookup "Content-Type")
+                        >>= maybeText
+                        >>= magicFileEnding
+                    )
+            )
+              -- Just the empty extension if we can’t figure it out.
+              & fromMaybe ""
+
+      body <- Wai.consumeRequestBodyStrict req
+      let hash :: Crypto.Digest Crypto.SHA256 = Crypto.hashlazy body
+      let hashBytes = hash & ByteArray.convert @(Crypto.Digest Crypto.SHA256) @ByteString
+      let len = ByteString.Lazy.length body
+      name <- getNameFromWordlist env
+      let fullname = name <> extension
+
+      let conn = env & envData
+      Sqlite.withTransaction conn $ do
+        Sqlite.executeNamed
+          conn
+          [Sqlite.sql|
+            INSERT INTO file_content
+              (content, hash_sha256, size)
+              VALUES
+              (:content, :hash_sha256, :size)
+              ON CONFLICT (hash_sha256) DO NOTHING
+          |]
+          [ ":content" := (body :: Lazy.ByteString),
+            ":hash_sha256" := (hashBytes :: ByteString),
+            ":size" := (len :: Int64)
+          ]
+
+        -- TODO: we are not checking if the name already exists,
+        -- we just assume that 1633^3 is enough to not get any collisions for now.
+        -- If the name exists, the user gets a 500.
+        Sqlite.executeNamed
+          conn
+          [Sqlite.sql|
+            INSERT INTO file_references
+              (file_content, reference_type, name, extension, mimetype)
+            VALUES
+              (:file_content, :reference_type, :name, :extension, :mimetype)
+          |]
+          [ ":file_content" := (hashBytes :: ByteString),
+            ":reference_type" := ("by-id" :: Text),
+            ":name" := name,
+            ":extension" := (extension :: Text),
+            ":mimetype" := (mimeType :: Text)
+          ]
+      pure $
+        Right
+          ( [("Content-Type", "text/plain")],
+            [fmt|/v0/by-id/{fullname}|]
+          )
+    _ -> Nothing
+
+-- Get a random name from a wordlist, that is three words connected by @-@.
+getNameFromWordlist :: Env -> IO Text
+getNameFromWordlist env =
+  do
+    let numberOfWords = 3 :: Int
+    Sqlite.queryNamed @(Sqlite.Only Text)
+      (env & envWordlist)
+      [Sqlite.sql|SELECT word FROM wordlist ORDER BY RANDOM() LIMIT :words|]
+      [":words" Sqlite.:= numberOfWords]
+    <&> map Sqlite.fromOnly
+    <&> Text.intercalate "-"
+
+-- | We can use a Rec with a named list of types to parse a returning row of sqlite!!
+instance
+  ( Sqlite.FromField t1,
+    Sqlite.FromField t2,
+    Sqlite.FromField t3
+  ) =>
+  Sqlite.FromRow (T3 l1 t1 l2 t2 l3 t3)
+  where
+  fromRow = do
+    T3
+      <$> (label @l1 <$> Sqlite.field)
+      <*> (label @l2 <$> Sqlite.field)
+      <*> (label @l3 <$> Sqlite.field)
diff --git a/users/Profpatsch/cas-serve/cas-serve.cabal b/users/Profpatsch/cas-serve/cas-serve.cabal
new file mode 100644
index 000000000000..3d988e42273c
--- /dev/null
+++ b/users/Profpatsch/cas-serve/cas-serve.cabal
@@ -0,0 +1,23 @@
+cabal-version:      2.4
+name:               cas-serve
+version:            0.1.0.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+executable cas-serve
+    main-is:          CasServe.hs
+
+    build-depends:
+        base >=4.15 && <5,
+        text,
+        sqlite-simple,
+        http-types,
+        wai,
+        warp,
+        mtl,
+        my-prelude,
+        bytestring,
+        memory,
+        cryptonite,
+
+    default-language: Haskell2010
diff --git a/users/Profpatsch/cas-serve/default.nix b/users/Profpatsch/cas-serve/default.nix
new file mode 100644
index 000000000000..6e4bfd324233
--- /dev/null
+++ b/users/Profpatsch/cas-serve/default.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  cas-serve = pkgs.writers.writeHaskell "cas-serve"
+    {
+      libraries = [
+        pkgs.haskellPackages.wai
+        pkgs.haskellPackages.warp
+        pkgs.haskellPackages.sqlite-simple
+        depot.users.Profpatsch.my-prelude
+      ];
+      ghcArgs = [ "-threaded" ];
+
+    } ./CasServe.hs;
+
+in
+cas-serve
diff --git a/users/Profpatsch/cas-serve/schema.sql b/users/Profpatsch/cas-serve/schema.sql
new file mode 100644
index 000000000000..b61a7a1ad57d
--- /dev/null
+++ b/users/Profpatsch/cas-serve/schema.sql
@@ -0,0 +1,38 @@
+-- SQLite
+.dump
+
+PRAGMA foreign_keys = ON;
+
+BEGIN transaction;
+
+create table if not exists file_content (
+  content blob NOT NULL,
+  hash_sha256 blob PRIMARY KEY,
+  size integer NOT NULL
+) WITHOUT ROWID;
+
+
+create table if not exists file_references (
+  rowid integer PRIMARY KEY,
+  file_content NOT NULL REFERENCES file_content ON DELETE CASCADE,
+  reference_type text NOT NULL,
+  name text NOT NULL,
+  extension text NOT NULL,
+  mimetype text NOT NULL
+);
+
+create unique index if not exists file_references_type_name_unique on file_references (reference_type, name);
+
+-- insert into file_content values ('mycontent', 'myhash', 9);
+-- insert into file_references values (NULL, 'myhash', 'by-id', 'myschranz', '.txt', 'text/plain');
+-- insert into file_content values (readfile('/home/philip/Pictures/screenshot.png'), 'anotherhash', 999);
+-- insert into file_references values (NULL, 'anotherhash', 'by-id', 'img', '.png', 'image/png');
+
+select * from file_content;
+
+select * from file_references;
+
+COMMIT;
+
+-- drop table file_content;
+-- drop table file_references;
diff --git a/users/Profpatsch/cas-serve/wordlist.json b/users/Profpatsch/cas-serve/wordlist.json
new file mode 100644
index 000000000000..cc4bc62ad153
--- /dev/null
+++ b/users/Profpatsch/cas-serve/wordlist.json
@@ -0,0 +1 @@
+ [ "acrobat", "africa", "alaska", "albert", "albino", "album", "alcohol", "alex", "alpha", "amadeus", "amanda", "amazon", "america", "analog", "animal", "antenna", "antonio", "apollo", "april", "aroma", "artist", "aspirin", "athlete", "atlas", "banana", "bandit", "banjo", "bikini", "bingo", "bonus", "camera", "canada", "carbon", "casino", "catalog", "cinema", "citizen", "cobra", "comet", "compact", "complex", "context", "credit", "critic", "crystal", "culture", "david", "delta", "dialog", "diploma", "doctor", "domino", "dragon", "drama", "extra", "fabric", "final", "focus", "forum", "galaxy", "gallery", "global", "harmony", "hotel", "humor", "index", "japan", "kilo", "lemon", "liter", "lotus", "mango", "melon", "menu", "meter", "metro", "mineral", "model", "music", "object", "piano", "pirate", "plastic", "radio", "report", "signal", "sport", "studio", "subject", "super", "tango", "taxi", "tempo", "tennis", "textile", "tokyo", "total", "tourist", "video", "visa", "academy", "alfred", "atlanta", "atomic", "barbara", "bazaar", "brother", "budget", "cabaret", "cadet", "candle", "capsule", "caviar", "channel", "chapter", "circle", "cobalt", "comrade", "condor", "crimson", "cyclone", "darwin", "declare", "denver", "desert", "divide", "dolby", "domain", "double", "eagle", "echo", "eclipse", "editor", "educate", "edward", "effect", "electra", "emerald", "emotion", "empire", "eternal", "evening", "exhibit", "expand", "explore", "extreme", "ferrari", "forget", "freedom", "friday", "fuji", "galileo", "genesis", "gravity", "habitat", "hamlet", "harlem", "helium", "holiday", "hunter", "ibiza", "iceberg", "imagine", "infant", "isotope", "jackson", "jamaica", "jasmine", "java", "jessica", "kitchen", "lazarus", "letter", "license", "lithium", "loyal", "lucky", "magenta", "manual", "marble", "maxwell", "mayor", "monarch", "monday", "money", "morning", "mother", "mystery", "native", "nectar", "nelson", "network", "nikita", "nobel", "nobody", "nominal", "norway", "nothing", "number", "october", "office", "oliver", "opinion", "option", "order", "outside", "package", "pandora", "panther", "papa", "pattern", "pedro", "pencil", "people", "phantom", "philips", "pioneer", "pluto", "podium", "portal", "potato", "process", "proxy", "pupil", "python", "quality", "quarter", "quiet", "rabbit", "radical", "radius", "rainbow", "ramirez", "ravioli", "raymond", "respect", "respond", "result", "resume", "richard", "river", "roger", "roman", "rondo", "sabrina", "salary", "salsa", "sample", "samuel", "saturn", "savage", "scarlet", "scorpio", "sector", "serpent", "shampoo", "sharon", "silence", "simple", "society", "sonar", "sonata", "soprano", "sparta", "spider", "sponsor", "abraham", "action", "active", "actor", "adam", "address", "admiral", "adrian", "agenda", "agent", "airline", "airport", "alabama", "aladdin", "alarm", "algebra", "alibi", "alice", "alien", "almond", "alpine", "amber", "amigo", "ammonia", "analyze", "anatomy", "angel", "annual", "answer", "apple", "archive", "arctic", "arena", "arizona", "armada", "arnold", "arsenal", "arthur", "asia", "aspect", "athena", "audio", "august", "austria", "avenue", "average", "axiom", "aztec", "bagel", "baker", "balance", "ballad", "ballet", "bambino", "bamboo", "baron", "basic", "basket", "battery", "belgium", "benefit", "berlin", "bermuda", "bernard", "bicycle", "binary", "biology", "bishop", "blitz", "block", "blonde", "bonjour", "boris", "boston", "bottle", "boxer", "brandy", "bravo", "brazil", "bridge", "british", "bronze", "brown", "bruce", "bruno", "brush", "burger", "burma", "cabinet", "cactus", "cafe", "cairo", "calypso", "camel", "campus", "canal", "cannon", "canoe", "cantina", "canvas", "canyon", "capital", "caramel", "caravan", "career", "cargo", "carlo", "carol", "carpet", "cartel", "cartoon", "castle", "castro", "cecilia", "cement", "center", "century", "ceramic", "chamber", "chance", "change", "chaos", "charlie", "charm", "charter", "cheese", "chef", "chemist", "cherry", "chess", "chicago", "chicken", "chief", "china", "cigar", "circus", "city", "clara", "classic", "claudia", "clean", "client", "climax", "clinic", "clock", "club", "cockpit", "coconut", "cola", "collect", "colombo", "colony", "color", "combat", "comedy", "command", "company", "concert", "connect", "consul", "contact", "contour", "control", "convert", "copy", "corner", "corona", "correct", "cosmos", "couple", "courage", "cowboy", "craft", "crash", "cricket", "crown", "cuba", "dallas", "dance", "daniel", "decade", "decimal", "degree", "delete", "deliver", "delphi", "deluxe", "demand", "demo", "denmark", "derby", "design", "detect", "develop", "diagram", "diamond", "diana", "diego", "diesel", "diet", "digital", "dilemma", "direct", "disco", "disney", "distant", "dollar", "dolphin", "donald", "drink", "driver", "dublin", "duet", "dynamic", "earth", "east", "ecology", "economy", "edgar", "egypt", "elastic", "elegant", "element", "elite", "elvis", "email", "empty", "energy", "engine", "english", "episode", "equator", "escape", "escort", "ethnic", "europe", "everest", "evident", "exact", "example", "exit", "exotic", "export", "express", "factor", "falcon", "family", "fantasy", "fashion", "fiber", "fiction", "fidel", "fiesta", "figure", "film", "filter", "finance", "finish", "finland", "first", "flag", "flash", "florida", "flower", "fluid", "flute", "folio", "ford", "forest", "formal", "formula", "fortune", "forward", "fragile", "france", "frank", "fresh", "friend", "frozen", "future", "gabriel", "gamma", "garage", "garcia", "garden", "garlic", "gemini", "general", "genetic", "genius", "germany", "gloria", "gold", "golf", "gondola", "gong", "good", "gordon", "gorilla", "grand", "granite", "graph", "green", "group", "guide", "guitar", "guru", "hand", "happy", "harbor", "harvard", "havana", "hawaii", "helena", "hello", "henry", "hilton", "history", "horizon", "house", "human", "icon", "idea", "igloo", "igor", "image", "impact", "import", "india", "indigo", "input", "insect", "instant", "iris", "italian", "jacket", "jacob", "jaguar", "janet", "jargon", "jazz", "jeep", "john", "joker", "jordan", "judo", "jumbo", "june", "jungle", "junior", "jupiter", "karate", "karma", "kayak", "kermit", "king", "koala", "korea", "labor", "lady", "lagoon", "laptop", "laser", "latin", "lava", "lecture", "left", "legal", "level", "lexicon", "liberal", "libra", "lily", "limbo", "limit", "linda", "linear", "lion", "liquid", "little", "llama", "lobby", "lobster", "local", "logic", "logo", "lola", "london", "lucas", "lunar", "machine", "macro", "madam", "madonna", "madrid", "maestro", "magic", "magnet", "magnum", "mailbox", "major", "mama", "mambo", "manager", "manila", "marco", "marina", "market", "mars", "martin", "marvin", "mary", "master", "matrix", "maximum", "media", "medical", "mega", "melody", "memo", "mental", "mentor", "mercury", "message", "metal", "meteor", "method", "mexico", "miami", "micro", "milk", "million", "minimum", "minus", "minute", "miracle", "mirage", "miranda", "mister", "mixer", "mobile", "modem", "modern", "modular", "moment", "monaco", "monica", "monitor", "mono", "monster", "montana", "morgan", "motel", "motif", "motor", "mozart", "multi", "museum", "mustang", "natural", "neon", "nepal", "neptune", "nerve", "neutral", "nevada", "news", "next", "ninja", "nirvana", "normal", "nova", "novel", "nuclear", "numeric", "nylon", "oasis", "observe", "ocean", "octopus", "olivia", "olympic", "omega", "opera", "optic", "optimal", "orange", "orbit", "organic", "orient", "origin", "orlando", "oscar", "oxford", "oxygen", "ozone", "pablo", "pacific", "pagoda", "palace", "pamela", "panama", "pancake", "panda", "panel", "panic", "paradox", "pardon", "paris", "parker", "parking", "parody", "partner", "passage", "passive", "pasta", "pastel", "patent", "patient", "patriot", "patrol", "pegasus", "pelican", "penguin", "pepper", "percent", "perfect", "perfume", "period", "permit", "person", "peru", "phone", "photo", "picasso", "picnic", "picture", "pigment", "pilgrim", "pilot", "pixel", "pizza", "planet", "plasma", "plaza", "pocket", "poem", "poetic", "poker", "polaris", "police", "politic", "polo", "polygon", "pony", "popcorn", "popular", "postage", "precise", "prefix", "premium", "present", "price", "prince", "printer", "prism", "private", "prize", "product", "profile", "program", "project", "protect", "proton", "public", "pulse", "puma", "pump", "pyramid", "queen", "radar", "ralph", "random", "rapid", "rebel", "record", "recycle", "reflex", "reform", "regard", "regular", "relax", "reptile", "reverse", "ricardo", "right", "ringo", "risk", "ritual", "robert", "robot", "rocket", "rodeo", "romeo", "royal", "russian", "safari", "salad", "salami", "salmon", "salon", "salute", "samba", "sandra", "santana", "sardine", "school", "scoop", "scratch", "screen", "script", "scroll", "second", "secret", "section", "segment", "select", "seminar", "senator", "senior", "sensor", "serial", "service", "shadow", "sharp", "sheriff", "shock", "short", "shrink", "sierra", "silicon", "silk", "silver", "similar", "simon", "single", "siren", "slang", "slogan", "smart", "smoke", "snake", "social", "soda", "solar", "solid", "solo", "sonic", "source", "soviet", "special", "speed", "sphere", "spiral", "spirit", "spring", "static", "status", "stereo", "stone", "stop", "street", "strong", "student", "style", "sultan", "susan", "sushi", "suzuki", "switch", "symbol", "system", "tactic", "tahiti", "talent", "tarzan", "telex", "texas", "theory", "thermos", "tiger", "titanic", "tomato", "topic", "tornado", "toronto", "torpedo", "totem", "tractor", "traffic", "transit", "trapeze", "travel", "tribal", "trick", "trident", "trilogy", "tripod", "tropic", "trumpet", "tulip", "tuna", "turbo", "twist", "ultra", "uniform", "union", "uranium", "vacuum", "valid", "vampire", "vanilla", "vatican", "velvet", "ventura", "venus", "vertigo", "veteran", "victor", "vienna", "viking", "village", "vincent", "violet", "violin", "virtual", "virus", "vision", "visitor", "visual", "vitamin", "viva", "vocal", "vodka", "volcano", "voltage", "volume", "voyage", "water", "weekend", "welcome", "western", "window", "winter", "wizard", "wolf", "world", "xray", "yankee", "yoga", "yogurt", "yoyo", "zebra", "zero", "zigzag", "zipper", "zodiac", "zoom", "acid", "adios", "agatha", "alamo", "alert", "almanac", "aloha", "andrea", "anita", "arcade", "aurora", "avalon", "baby", "baggage", "balloon", "bank", "basil", "begin", "biscuit", "blue", "bombay", "botanic", "brain", "brenda", "brigade", "cable", "calibre", "carmen", "cello", "celtic", "chariot", "chrome", "citrus", "civil", "cloud", "combine", "common", "cool", "copper", "coral", "crater", "cubic", "cupid", "cycle", "depend", "door", "dream", "dynasty", "edison", "edition", "enigma", "equal", "eric", "event", "evita", "exodus", "extend", "famous", "farmer", "food", "fossil", "frog", "fruit", "geneva", "gentle", "george", "giant", "gilbert", "gossip", "gram", "greek", "grille", "hammer", "harvest", "hazard", "heaven", "herbert", "heroic", "hexagon", "husband", "immune", "inca", "inch", "initial", "isabel", "ivory", "jason", "jerome", "joel", "joshua", "journal", "judge", "juliet", "jump", "justice", "kimono", "kinetic", "leonid", "leopard", "lima", "maze", "medusa", "member", "memphis", "michael", "miguel", "milan", "mile", "miller", "mimic", "mimosa", "mission", "monkey", "moral", "moses", "mouse", "nancy", "natasha", "nebula", "nickel", "nina", "noise", "orchid", "oregano", "origami", "orinoco", "orion", "othello", "paper", "paprika", "prelude", "prepare", "pretend", "promise", "prosper", "provide", "puzzle", "remote", "repair", "reply", "rival", "riviera", "robin", "rose", "rover", "rudolf", "saga", "sahara", "scholar", "shelter", "ship", "shoe", "sigma", "sister", "sleep", "smile", "spain", "spark", "split", "spray", "square", "stadium", "star", "storm", "story", "strange", "stretch", "stuart", "subway", "sugar", "sulfur", "summer", "survive", "sweet", "swim", "table", "taboo", "target", "teacher", "telecom", "temple", "tibet", "ticket", "tina", "today", "toga", "tommy", "tower", "trivial", "tunnel", "turtle", "twin", "uncle", "unicorn", "unique", "update", "valery", "vega", "version", "voodoo", "warning", "william", "wonder", "year", "yellow", "young", "absent", "absorb", "absurd", "accent", "alfonso", "alias", "ambient", "anagram", "andy", "anvil", "appear", "apropos", "archer", "ariel", "armor", "arrow", "austin", "avatar", "axis", "baboon", "bahama", "bali", "balsa", "barcode", "bazooka", "beach", "beast", "beatles", "beauty", "before", "benny", "betty", "between", "beyond", "billy", "bison", "blast", "bless", "bogart", "bonanza", "book", "border", "brave", "bread", "break", "broken", "bucket", "buenos", "buffalo", "bundle", "button", "buzzer", "byte", "caesar", "camilla", "canary", "candid", "carrot", "cave", "chant", "child", "choice", "chris", "cipher", "clarion", "clark", "clever", "cliff", "clone", "conan", "conduct", "congo", "costume", "cotton", "cover", "crack", "current", "danube", "data", "decide", "deposit", "desire", "detail", "dexter", "dinner", "donor", "druid", "drum", "easy", "eddie", "enjoy", "enrico", "epoxy", "erosion", "except", "exile", "explain", "fame", "fast", "father", "felix", "field", "fiona", "fire", "fish", "flame", "flex", "flipper", "float", "flood", "floor", "forbid", "forever", "fractal", "frame", "freddie", "front", "fuel", "gallop", "game", "garbo", "gate", "gelatin", "gibson", "ginger", "giraffe", "gizmo", "glass", "goblin", "gopher", "grace", "gray", "gregory", "grid", "griffin", "ground", "guest", "gustav", "gyro", "hair", "halt", "harris", "heart", "heavy", "herman", "hippie", "hobby", "honey", "hope", "horse", "hostel", "hydro", "imitate", "info", "ingrid", "inside", "invent", "invest", "invite", "ivan", "james", "jester", "jimmy", "join", "joseph", "juice", "julius", "july", "kansas", "karl", "kevin", "kiwi", "ladder", "lake", "laura", "learn", "legacy", "legend", "lesson", "life", "light", "list", "locate", "lopez", "lorenzo", "love", "lunch", "malta", "mammal", "margin", "margo", "marion", "mask", "match", "mayday", "meaning", "mercy", "middle", "mike", "mirror", "modest", "morph", "morris", "mystic", "nadia", "nato", "navy", "needle", "neuron", "never", "newton", "nice", "night", "nissan", "nitro", "nixon", "north", "oberon", "octavia", "ohio", "olga", "open", "opus", "orca", "oval", "owner", "page", "paint", "palma", "parent", "parlor", "parole", "paul", "peace", "pearl", "perform", "phoenix", "phrase", "pierre", "pinball", "place", "plate", "plato", "plume", "pogo", "point", "polka", "poncho", "powder", "prague", "press", "presto", "pretty", "prime", "promo", "quest", "quick", "quiz", "quota", "race", "rachel", "raja", "ranger", "region", "remark", "rent", "reward", "rhino", "ribbon", "rider", "road", "rodent", "round", "rubber", "ruby", "rufus", "sabine", "saddle", "sailor", "saint", "salt", "scale", "scuba", "season", "secure", "shake", "shallow", "shannon", "shave", "shelf", "sherman", "shine", "shirt", "side", "sinatra", "sincere", "size", "slalom", "slow", "small", "snow", "sofia", "song", "sound", "south", "speech", "spell", "spend", "spoon", "stage", "stamp", "stand", "state", "stella", "stick", "sting", "stock", "store", "sunday", "sunset", "support", "supreme", "sweden", "swing", "tape", "tavern", "think", "thomas", "tictac", "time", "toast", "tobacco", "tonight", "torch", "torso", "touch", "toyota", "trade", "tribune", "trinity", "triton", "truck", "trust", "type", "under", "unit", "urban", "urgent", "user", "value", "vendor", "venice", "verona", "vibrate", "virgo", "visible", "vista", "vital", "voice", "vortex", "waiter", "watch", "wave", "weather", "wedding", "wheel", "whiskey", "wisdom", "android", "annex", "armani", "cake", "confide", "deal", "define", "dispute", "genuine", "idiom", "impress", "include", "ironic", "null", "nurse", "obscure", "prefer", "prodigy", "ego", "fax", "jet", "job", "rio", "ski", "yes" ]
diff --git a/users/Profpatsch/cas-serve/wordlist.sqlite b/users/Profpatsch/cas-serve/wordlist.sqlite
new file mode 100644
index 000000000000..5074474ba0a6
--- /dev/null
+++ b/users/Profpatsch/cas-serve/wordlist.sqlite
Binary files differdiff --git a/users/Profpatsch/cdb.nix b/users/Profpatsch/cdb.nix
new file mode 100644
index 000000000000..86e0a2d58f24
--- /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/dhall/lib.dhall b/users/Profpatsch/dhall/lib.dhall
new file mode 100644
index 000000000000..fb97ba6070ef
--- /dev/null
+++ b/users/Profpatsch/dhall/lib.dhall
@@ -0,0 +1,84 @@
+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))
+          )
+
+let
+
+    --| Concatenate a `List` of `List`s into a single `List`
+    List/concat
+    : ∀(a : Type) → List (List a) → List a
+    = λ(a : Type) →
+      λ(xss : List (List a)) →
+        List/build
+          a
+          ( λ(list : Type) →
+            λ(cons : a → list → list) →
+            λ(nil : list) →
+              List/fold
+                (List a)
+                xss
+                list
+                (λ(xs : List a) → λ(ys : list) → List/fold a xs list cons ys)
+                nil
+          )
+
+let
+
+
+    -- Transform a list by applying a function to each element and flattening the results
+    List/concatMap
+    : ∀(a : Type) → ∀(b : Type) → (a → List b) → List a → List b
+    = λ(a : Type) →
+      λ(b : Type) →
+      λ(f : a → List b) →
+      λ(xs : List a) →
+        List/build
+          b
+          ( λ(list : Type) →
+            λ(cons : b → list → list) →
+              List/fold a xs list (λ(x : a) → List/fold b (f x) list cons)
+          )
+
+let Status = < Empty | NonEmpty : Text >
+
+let
+
+    {-|
+    Transform each value in a `List` to `Text` and then concatenate them with a
+    separator in between each value
+    -}
+    Text/concatMapSep
+    : ∀(separator : Text) → ∀(a : Type) → (a → Text) → List a → Text
+    = λ(separator : Text) →
+      λ(a : Type) →
+      λ(f : a → Text) →
+      λ(elements : List a) →
+        let status =
+              List/fold
+                a
+                elements
+                Status
+                ( λ(x : a) →
+                  λ(status : Status) →
+                    merge
+                      { Empty = Status.NonEmpty (f x)
+                      , NonEmpty =
+                          λ(result : Text) →
+                            Status.NonEmpty (f x ++ separator ++ result)
+                      }
+                      status
+                )
+                Status.Empty
+
+        in  merge { Empty = "", NonEmpty = λ(result : Text) → result } status
+
+in  { List/map, List/concat, List/concatMap, Text/concatMapSep }
diff --git a/users/Profpatsch/emacs-tree-sitter-move/README.md b/users/Profpatsch/emacs-tree-sitter-move/README.md
new file mode 100644
index 000000000000..ae8d763d61c8
--- /dev/null
+++ b/users/Profpatsch/emacs-tree-sitter-move/README.md
@@ -0,0 +1,5 @@
+# emacs-tree-sitter-move
+
+An experiment in whether we can implement structural editing in emacs using the tree-sitter parser.
+
+What currently works: loading a tree-sitter gramma, navigating the AST left/right/up/down.
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 000000000000..a9f259d96d20
--- /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 000000000000..f400d5c02161
--- /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 000000000000..d9f8075976d6
--- /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 000000000000..0f57bae035da
--- /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 000000000000..681081f5909d
--- /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 000000000000..88d13fa45b81
--- /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 000000000000..907e1e4081bc
--- /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 000000000000..5c713b5b1c84
--- /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/ExecHelpers.hs b/users/Profpatsch/execline/ExecHelpers.hs
new file mode 100644
index 000000000000..438047b2b957
--- /dev/null
+++ b/users/Profpatsch/execline/ExecHelpers.hs
@@ -0,0 +1,48 @@
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TypeApplications #-}
+
+module ExecHelpers where
+
+import Data.String (IsString)
+import MyPrelude
+import qualified System.Exit as Sys
+
+newtype CurrentProgramName = CurrentProgramName { unCurrentProgramName :: Text }
+  deriving newtype (Show, Eq, Ord, IsString)
+
+-- | Exit 1 to signify a generic expected error
+-- (e.g. something that sometimes just goes wrong, like a nix build).
+dieExpectedError :: CurrentProgramName -> Text -> IO a
+dieExpectedError = dieWith 1
+
+-- | 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.
+dieUserError :: CurrentProgramName -> Text -> IO a
+dieUserError = dieWith 100
+
+-- |  Exit 101 to signify an unexpected crash (failing assertion or panic).
+diePanic :: CurrentProgramName -> Text -> IO a
+diePanic = dieWith 101
+
+-- | Exit 111 to signify a temporary error (such as resource exhaustion)
+dieTemporary :: CurrentProgramName -> Text -> IO a
+dieTemporary = dieWith 111
+
+-- |  Exit 126 to signify an environment problem
+-- (the user has set up stuff incorrectly so the program cannot work)
+dieEnvironmentProblem :: CurrentProgramName -> Text -> IO a
+dieEnvironmentProblem = dieWith 126
+
+-- | Exit 127 to signify a missing executable.
+dieMissingExecutable :: CurrentProgramName -> Text -> IO a
+dieMissingExecutable = dieWith 127
+
+dieWith :: Natural -> CurrentProgramName -> Text -> IO a
+dieWith status currentProgramName msg = do
+  putStderrLn [fmt|{currentProgramName & unCurrentProgramName}: {msg}|]
+  Sys.exitWith
+    (Sys.ExitFailure (status & fromIntegral @Natural @Int))
diff --git a/users/Profpatsch/execline/default.nix b/users/Profpatsch/execline/default.nix
new file mode 100644
index 000000000000..47c7f8b74971
--- /dev/null
+++ b/users/Profpatsch/execline/default.nix
@@ -0,0 +1,48 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  exec-helpers-hs = pkgs.haskellPackages.mkDerivation {
+    pname = "exec-helpers";
+    version = "0.1.0";
+
+    src = depot.users.Profpatsch.exactSource ./. [
+      ./exec-helpers.cabal
+      ./ExecHelpers.hs
+    ];
+
+    libraryHaskellDepends = [
+      depot.users.Profpatsch.my-prelude
+    ];
+
+    isLibrary = true;
+    license = lib.licenses.mit;
+  };
+
+  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-hs
+    print-one-env
+    ;
+}
diff --git a/users/Profpatsch/execline/exec-helpers.cabal b/users/Profpatsch/execline/exec-helpers.cabal
new file mode 100644
index 000000000000..1262a36ec23b
--- /dev/null
+++ b/users/Profpatsch/execline/exec-helpers.cabal
@@ -0,0 +1,14 @@
+cabal-version:      2.4
+name:               exec-helpers
+version:            0.1.0.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+library
+    exposed-modules:          ExecHelpers
+
+    build-depends:
+        base >=4.15 && <5,
+        my-prelude
+
+    default-language: Haskell2010
diff --git a/users/Profpatsch/execline/exec-helpers/Cargo.lock b/users/Profpatsch/execline/exec-helpers/Cargo.lock
new file mode 100644
index 000000000000..1753cc949d3a
--- /dev/null
+++ b/users/Profpatsch/execline/exec-helpers/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "exec_helpers"
+version = "0.1.0"
diff --git a/users/Profpatsch/execline/exec-helpers/Cargo.toml b/users/Profpatsch/execline/exec-helpers/Cargo.toml
new file mode 100644
index 000000000000..6642b66ee375
--- /dev/null
+++ b/users/Profpatsch/execline/exec-helpers/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "exec_helpers"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+name = "exec_helpers"
+path = "exec_helpers.rs"
diff --git a/users/Profpatsch/execline/exec-helpers/default.nix b/users/Profpatsch/execline/exec-helpers/default.nix
new file mode 100644
index 000000000000..5545d41d9de7
--- /dev/null
+++ b/users/Profpatsch/execline/exec-helpers/default.nix
@@ -0,0 +1,6 @@
+{ depot, ... }:
+depot.nix.writers.rustSimpleLib
+{
+  name = "exec-helpers";
+}
+  (builtins.readFile ./exec_helpers.rs)
diff --git a/users/Profpatsch/execline/exec-helpers/exec_helpers.rs b/users/Profpatsch/execline/exec-helpers/exec_helpers.rs
new file mode 100644
index 000000000000..a57cbca35391
--- /dev/null
+++ b/users/Profpatsch/execline/exec-helpers/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/fafo.jpg b/users/Profpatsch/fafo.jpg
new file mode 100644
index 000000000000..78f11d208e1f
--- /dev/null
+++ b/users/Profpatsch/fafo.jpg
Binary files differdiff --git a/users/Profpatsch/git-db/default.nix b/users/Profpatsch/git-db/default.nix
new file mode 100644
index 000000000000..ad5d927677bf
--- /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 000000000000..c8019bf03661
--- /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/haskell-module-deps/README.md b/users/Profpatsch/haskell-module-deps/README.md
new file mode 100644
index 000000000000..b4f35beac520
--- /dev/null
+++ b/users/Profpatsch/haskell-module-deps/README.md
@@ -0,0 +1,5 @@
+# haskell-module-deps
+
+An executable that when run in a project directory containing `.hs` files in `./src` will output a png/graph of how those modules import each other, transitively.
+
+Useful for getting an overview, finding weird import edges, figuring out how to get more compilation parallelism into your Haskell project.
diff --git a/users/Profpatsch/haskell-module-deps/default.nix b/users/Profpatsch/haskell-module-deps/default.nix
new file mode 100644
index 000000000000..71cc0a5b0d31
--- /dev/null
+++ b/users/Profpatsch/haskell-module-deps/default.nix
@@ -0,0 +1,55 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.zathura [ "zathura" ]
+    // depot.nix.getBins pkgs.haskellPackages.graphmod [ "graphmod" ]
+    // depot.nix.getBins pkgs.graphviz [ "dot" ]
+  ;
+
+  # Display a graph of all modules in a project and how they depend on each other.
+  # Takes the project directory as argument.
+  # Open in zathura.
+  haskell-module-deps = depot.nix.writeExecline "haskell-module-deps" { } [
+    "pipeline"
+    [ haskell-module-deps-with-filetype "pdf" "$@" ]
+    bins.zathura
+    "-"
+  ];
+
+  # Display a graph of all modules in a project and how they depend on each other.
+  # Takes the project directory as argument.
+  # Print a png to stdout.
+  haskell-module-deps-png = depot.nix.writeExecline "haskell-module-deps-png" { } [
+    haskell-module-deps-with-filetype
+    "png"
+    "$@"
+  ];
+
+  # Display a graph of all modules in a project and how they depend on each other.
+  # Takes the file type to generate as first argument
+  # and the project directory as second argument.
+  haskell-module-deps-with-filetype = pkgs.writers.writeBash "haskell-module-deps-with-filetype" ''
+    set -euo pipefail
+    shopt -s globstar
+    filetype="$1"
+    rootDir="$2"
+    ${bins.graphmod} \
+      ${/*silence warnings for missing external dependencies*/""} \
+      --quiet \
+      ${/*applies some kind of import simplification*/""} \
+      --prune-edges \
+      "$rootDir"/src/**/*.hs \
+      | ${bins.dot} \
+          ${/*otherwise it’s a bit cramped*/""} \
+          -Gsize="20,20!" \
+          -T"$filetype"
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  inherit
+    haskell-module-deps
+    haskell-module-deps-png
+    haskell-module-deps-with-filetype
+    ;
+}
diff --git a/users/Profpatsch/haskell-module-deps/example-output-dhall-haskell.png b/users/Profpatsch/haskell-module-deps/example-output-dhall-haskell.png
new file mode 100644
index 000000000000..53725c49e8d1
--- /dev/null
+++ b/users/Profpatsch/haskell-module-deps/example-output-dhall-haskell.png
Binary files differdiff --git a/users/Profpatsch/ical-smolify/IcalSmolify.hs b/users/Profpatsch/ical-smolify/IcalSmolify.hs
new file mode 100644
index 000000000000..77264d16937e
--- /dev/null
+++ b/users/Profpatsch/ical-smolify/IcalSmolify.hs
@@ -0,0 +1,124 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Main where
+
+import qualified Data.ByteString.Lazy as Bytes.Lazy
+import qualified Data.CaseInsensitive as CaseInsensitive
+import qualified Data.Default as Default
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import ExecHelpers (dieUserError, CurrentProgramName)
+import MyPrelude
+import qualified System.Environment as Env
+import Text.ICalendar
+import Prelude hiding (log)
+
+main :: IO ()
+main = do
+  Env.getArgs >>= \case
+    [] -> dieUserError progName "First argument must be the ics file name"
+    (file : _) ->
+      do
+        parse file
+        >>= traverse_
+          ( \vcal ->
+              vcal
+                & stripSingleTimezone
+                & minify
+                & printICalendar Default.def
+                & Bytes.Lazy.putStr
+          )
+
+progName :: CurrentProgramName
+progName = "ical-smolify"
+
+log :: Error -> IO ()
+log err = do
+  putStderrLn (errorContext "ical-smolify" err & prettyError)
+
+parse :: FilePath -> IO [VCalendar]
+parse file = do
+  parseICalendarFile Default.def file >>= \case
+    Left err -> do
+      dieUserError progName [fmt|Cannot parse ical file: {err}|]
+    Right (cals, warnings) -> do
+      for_ warnings (\warn -> log [fmt|Warning: {warn}|])
+      pure cals
+
+-- | Converts a single timezone definition to the corresponding X-WR-Timezone field.
+stripSingleTimezone :: VCalendar -> VCalendar
+stripSingleTimezone vcal =
+  case vcal & vcTimeZones & Map.toList of
+    [] -> vcal
+    [(_, tz)] -> do
+      let xtz =
+            OtherProperty
+              { otherName = CaseInsensitive.mk "X-WR-TIMEZONE",
+                otherValue = tz & vtzId & tzidValue & textToBytesUtf8Lazy,
+                otherParams = OtherParams Set.empty
+              }
+      vcal
+        { vcOther =
+            vcal & vcOther
+              -- remove any existing x-wr-timezone fields
+              & Set.filter (\prop -> (prop & otherName) /= (xtz & otherName))
+              & Set.insert xtz,
+          vcTimeZones = Map.empty
+        }
+    _more -> vcal
+
+-- | Minify the vcalendar event by throwing away everything that’s not an event.
+minify :: VCalendar -> VCalendar
+minify vcal =
+  vcal
+    { vcProdId = ProdId "" (OtherParams Set.empty),
+      -- , vcVersion    :: ICalVersion
+      -- , vcScale      :: Scale
+      -- , vcMethod     :: Maybe Method
+      -- , vcOther      :: …
+      -- , vcTimeZones  :: Map Text VTimeZone
+      vcEvents = Map.map minifyEvent (vcal & vcEvents),
+      vcTodos = Map.empty,
+      vcJournals = Map.empty,
+      vcFreeBusys = Map.empty,
+      vcOtherComps = Set.empty
+    }
+
+minifyEvent :: VEvent -> VEvent
+minifyEvent vev =
+  vev
+--  { veDTStamp       :: DTStamp
+--   , veUID           :: UID
+--   , veClass         :: Class -- ^ 'def' = 'Public'
+--   , veDTStart       :: Maybe DTStart
+--   , veCreated       :: Maybe Created
+--   , veDescription   :: Maybe Description
+--   , veGeo           :: Maybe Geo
+--   , veLastMod       :: Maybe LastModified
+--   , veLocation      :: Maybe Location
+--   , veOrganizer     :: Maybe Organizer
+--   , vePriority      :: Priority -- ^ 'def' = 0
+--   , veSeq           :: Sequence -- ^ 'def' = 0
+--   , veStatus        :: Maybe EventStatus
+--   , veSummary       :: Maybe Summary
+--   , veTransp        :: TimeTransparency -- ^ 'def' = 'Opaque'
+--   , veUrl           :: Maybe URL
+--   , veRecurId       :: Maybe RecurrenceId
+--   , veRRule         :: Set RRule
+--   , veDTEndDuration :: Maybe (Either DTEnd DurationProp)
+--   , veAttach        :: Set Attachment
+--   , veAttendee      :: Set Attendee
+--   , veCategories    :: Set Categories
+--   , veComment       :: Set Comment
+--   , veContact       :: Set Contact
+--   , veExDate        :: Set ExDate
+--   , veRStatus       :: Set RequestStatus
+--   , veRelated       :: Set RelatedTo
+--   , veResources     :: Set Resources
+--   , veRDate         :: Set RDate
+--   , veAlarms        :: Set VAlarm
+--   , veOther         :: Set OtherProperty
+--   }
diff --git a/users/Profpatsch/ical-smolify/README.md b/users/Profpatsch/ical-smolify/README.md
new file mode 100644
index 000000000000..86c166d3c179
--- /dev/null
+++ b/users/Profpatsch/ical-smolify/README.md
@@ -0,0 +1,5 @@
+# ical-smolify
+
+Ensmallen an `ical` by stripping out redundant information like timezone definitions.
+
+The idea here was that after running through this preprocessor, it fits into a QR code (~2000bits) that can be scanned with your phone (for automatically adding to mobile calendar).
diff --git a/users/Profpatsch/ical-smolify/default.nix b/users/Profpatsch/ical-smolify/default.nix
new file mode 100644
index 000000000000..f67d175ce3f3
--- /dev/null
+++ b/users/Profpatsch/ical-smolify/default.nix
@@ -0,0 +1,16 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  cas-serve = pkgs.writers.writeHaskell "ical-smolify"
+    {
+      libraries = [
+        pkgs.haskellPackages.iCalendar
+        depot.users.Profpatsch.my-prelude
+        depot.users.Profpatsch.execline.exec-helpers-hs
+
+      ];
+      ghcArgs = [ "-threaded" ];
+    } ./IcalSmolify.hs;
+
+in
+cas-serve
diff --git a/users/Profpatsch/ical-smolify/ical-smolify.cabal b/users/Profpatsch/ical-smolify/ical-smolify.cabal
new file mode 100644
index 000000000000..571b8267c8c3
--- /dev/null
+++ b/users/Profpatsch/ical-smolify/ical-smolify.cabal
@@ -0,0 +1,18 @@
+cabal-version:      2.4
+name:               ical-smolify
+version:            0.1.0.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+executable ical-smolify
+    main-is: IcalSmolify.hs
+
+    build-depends:
+        base >=4.15 && <5,
+        my-prelude,
+        exec-helpers
+        data-default
+        case-insensitive
+        iCalendar
+
+    default-language: Haskell2010
diff --git a/users/Profpatsch/imap-idle.nix b/users/Profpatsch/imap-idle.nix
new file mode 100644
index 000000000000..84af5d0e54a9
--- /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 000000000000..937847b8798a
--- /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 000000000000..1947ad1ce123
--- /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/ini/default.nix b/users/Profpatsch/ini/default.nix
new file mode 100644
index 000000000000..e1a7a1a7b6b7
--- /dev/null
+++ b/users/Profpatsch/ini/default.nix
@@ -0,0 +1,6 @@
+{ depot, ... }:
+{
+  externs = {
+    renderIni = depot.users.Profpatsch.toINI { };
+  };
+}
diff --git a/users/Profpatsch/ini/ini.dhall b/users/Profpatsch/ini/ini.dhall
new file mode 100644
index 000000000000..f2efbc0af4f1
--- /dev/null
+++ b/users/Profpatsch/ini/ini.dhall
@@ -0,0 +1,36 @@
+let lib = ../dhall/lib.dhall
+
+let NameVal = λ(T : Type) → { name : Text, value : T }
+
+let ValueList = λ(T : Type) → List (NameVal T)
+
+let Section = ValueList Text
+
+let Sections = ValueList Section
+
+let Ini = { globalSection : Section, sections : Sections }
+
+let
+    -- Takes to INI files and merges their global sections and their section lists,
+    -- without duplicating by section name.
+    appendInis =
+      λ(inis : List Ini) →
+          { globalSection =
+              lib.List/concat
+                (NameVal Text)
+                (lib.List/map Ini Section (λ(i : Ini) → i.globalSection) inis)
+          , sections =
+              lib.List/concat
+                (NameVal Section)
+                (lib.List/map Ini Sections (λ(i : Ini) → i.sections) inis)
+          }
+        : Ini
+
+let
+    -- Signatures of functions that are input via FFI.
+    Externs =
+      { -- given a dsl of functions to create an Ini, render the ini file
+        renderIni : Ini → Text
+      }
+
+in  { NameVal, ValueList, Section, Sections, Ini, appendInis, Externs }
diff --git a/users/Profpatsch/lens.nix b/users/Profpatsch/lens.nix
new file mode 100644
index 000000000000..28f7506bddae
--- /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 000000000000..879d87755d56
--- /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/lorri-wait-for-eval/LorriWaitForEval.hs b/users/Profpatsch/lorri-wait-for-eval/LorriWaitForEval.hs
new file mode 100644
index 000000000000..a1a45864019c
--- /dev/null
+++ b/users/Profpatsch/lorri-wait-for-eval/LorriWaitForEval.hs
@@ -0,0 +1,173 @@
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Main where
+
+import Conduit
+import Conduit qualified as Cond
+import Control.Concurrent
+import Control.Concurrent.Async qualified as Async
+import Control.Monad
+import Data.Aeson.BetterErrors qualified as Json
+import Data.Bifunctor
+import Data.Conduit.Binary qualified as Conduit.Binary
+import Data.Conduit.Combinators qualified as Cond
+import Data.Conduit.Process
+import Data.Error
+import Data.Function
+import Data.Functor
+import Data.Text.IO (hPutStrLn)
+import MyPrelude
+import System.Directory qualified as Dir
+import System.Environment qualified as Env
+import System.Exit qualified as Exit
+import System.FilePath (takeDirectory)
+import System.FilePath.Posix qualified as FilePath
+import System.IO (stderr)
+import System.Posix qualified as Posix
+import Prelude hiding (log)
+
+data LorriEvent = LorriEvent
+  { nixFile :: Text,
+    eventType :: LorriEventType
+  }
+  deriving stock (Show)
+
+data LorriEventType
+  = Completed
+  | Started
+  | EvalFailure
+  deriving stock (Show)
+
+main :: IO ()
+main = do
+  argv <- Env.getArgs <&> nonEmpty
+
+  dir <- Dir.getCurrentDirectory
+  shellNix <-
+    findShellNix dir >>= \case
+      Nothing -> Exit.die [fmt|could not find any shell.nix in or above the directory {dir}|]
+      Just s -> pure s
+  getEventChan :: MVar (Chan LorriEvent) <- newEmptyMVar
+  Async.race_
+    ( do
+        sendEventChan :: Chan LorriEvent <- newChan
+        (exitCode, ()) <-
+          sourceProcessWithConsumer
+            (proc "lorri" ["internal", "stream-events"])
+            $
+            -- first, we want to send a message over the chan that the process is running (for timeout)
+            liftIO (putMVar getEventChan sendEventChan)
+              *> Conduit.Binary.lines
+              .| Cond.mapC
+                ( \jsonBytes ->
+                    (jsonBytes :: ByteString)
+                      & Json.parseStrict
+                        ( Json.key
+                            "Completed"
+                            ( do
+                                nixFile <- Json.key "nix_file" Json.asText
+                                pure LorriEvent {nixFile, eventType = Completed}
+                            )
+                            Json.<|> Json.key
+                              "Started"
+                              ( do
+                                  nixFile <- Json.key "nix_file" Json.asText
+                                  pure LorriEvent {nixFile, eventType = Started}
+                              )
+                            Json.<|> Json.key
+                              "Failure"
+                              ( do
+                                  nixFile <- Json.key "nix_file" Json.asText
+                                  pure LorriEvent {nixFile, eventType = EvalFailure}
+                              )
+                        )
+                      & first Json.displayError'
+                      & first (map newError)
+                      & first (smushErrors [fmt|Cannot parse line returned by lorri: {jsonBytes & bytesToTextUtf8Lenient}|])
+                      & unwrapError
+                )
+              .| (Cond.mapM_ (\ev -> writeChan sendEventChan ev))
+
+        log [fmt|lorri internal stream-events exited {show exitCode}|]
+    )
+    ( do
+        let waitMs ms = threadDelay (ms * 1000)
+
+        -- log [fmt|Waiting for lorri event for {shellNix}|]
+
+        eventChan <- takeMVar getEventChan
+
+        let isOurEvent ev = FilePath.normalise (ev & nixFile & textToString) == FilePath.normalise shellNix
+
+        let handleEvent ev =
+              case ev & eventType of
+                Started ->
+                  log [fmt|waiting for lorri build to finish|]
+                Completed -> do
+                  log [fmt|build completed|]
+                  exec (inDirenvDir (takeDirectory shellNix) <$> argv)
+                EvalFailure -> do
+                  log [fmt|evaluation failed! for path {ev & nixFile}|]
+                  Exit.exitWith (Exit.ExitFailure 111)
+
+        -- wait for 100ms for the first message from lorri,
+        -- or else assume lorri is not building the project yet
+        Async.race
+          (waitMs 100)
+          ( do
+              -- find the first event that we can use
+              let go = do
+                    ev <- readChan eventChan
+                    if isOurEvent ev then pure ev else go
+              go
+          )
+          >>= \case
+            Left () -> do
+              log [fmt|No event received from lorri, assuming this is the first evaluation|]
+              exec argv
+            Right ch -> handleEvent ch
+
+        runConduit $
+          repeatMC (readChan eventChan)
+            .| filterC isOurEvent
+            .| mapM_C handleEvent
+    )
+  where
+    inDirenvDir dir' argv' = ("direnv" :| ["exec", dir']) <> argv'
+    exec = \case
+      Just (exe :| args') -> Posix.executeFile exe True args' Nothing
+      Nothing -> Exit.exitSuccess
+
+log :: Text -> IO ()
+log msg = hPutStrLn stderr [fmt|lorri-wait-for-eval: {msg}|]
+
+-- | Searches from the current directory upwards, until it finds the `shell.nix`.
+findShellNix :: FilePath -> IO (Maybe FilePath)
+findShellNix curDir = do
+  let go :: (FilePath -> IO (Maybe FilePath))
+      go dir = do
+        let file = dir FilePath.</> "shell.nix"
+        Dir.doesFileExist file >>= \case
+          True -> pure (Just file)
+          False -> do
+            let parent = FilePath.takeDirectory dir
+            if parent == dir
+              then pure Nothing
+              else go parent
+  go (FilePath.normalise curDir)
+
+smushErrors :: Foldable t => Text -> t Error -> Error
+smushErrors msg errs =
+  errs
+    -- hrm, pretty printing and then creating a new error is kinda shady
+    & foldMap (\err -> "\n- " <> prettyError err)
+    & newError
+    & errorContext msg
diff --git a/users/Profpatsch/lorri-wait-for-eval/README.md b/users/Profpatsch/lorri-wait-for-eval/README.md
new file mode 100644
index 000000000000..9c5d8ef9e321
--- /dev/null
+++ b/users/Profpatsch/lorri-wait-for-eval/README.md
@@ -0,0 +1,7 @@
+# lorri-wait-for-eval
+
+A helper script for [lorri](https://github.com/nix-community/lorri), which wraps a command and executes it once lorri is finished evaluating the current `shell.nix`, and uses the new environment.
+
+This is useful when you need the new shell environment to be in scope of the command, but don’t want to waste time waiting for it to finish.
+
+This should really be a feature of lorri, but I couldn’t be assed to touch rust :P
diff --git a/users/Profpatsch/lorri-wait-for-eval/default.nix b/users/Profpatsch/lorri-wait-for-eval/default.nix
new file mode 100644
index 000000000000..90c6365fed5a
--- /dev/null
+++ b/users/Profpatsch/lorri-wait-for-eval/default.nix
@@ -0,0 +1,20 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  lorri-wait-for-eval = pkgs.writers.writeHaskell "lorri-wait-for-eval"
+    {
+      libraries = [
+        depot.users.Profpatsch.my-prelude
+        pkgs.haskellPackages.async
+        pkgs.haskellPackages.aeson-better-errors
+        pkgs.haskellPackages.conduit-extra
+        pkgs.haskellPackages.error
+        pkgs.haskellPackages.PyF
+        pkgs.haskellPackages.unliftio
+      ];
+      ghcArgs = [ "-threaded" ];
+
+    } ./LorriWaitForEval.hs;
+
+in
+lorri-wait-for-eval
diff --git a/users/Profpatsch/mailbox-org/AesonQQ.hs b/users/Profpatsch/mailbox-org/AesonQQ.hs
new file mode 100644
index 000000000000..02e1c2f3dff0
--- /dev/null
+++ b/users/Profpatsch/mailbox-org/AesonQQ.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE TemplateHaskellQuotes #-}
+
+module AesonQQ where
+
+import Data.Aeson qualified as Json
+import Data.Either qualified as Either
+import MyPrelude
+import PyF qualified
+import PyF.Internal.QQ qualified as PyFConf
+
+aesonQQ =
+  PyF.mkFormatter
+    "aesonQQ"
+    PyF.defaultConfig
+      { PyFConf.delimiters = Just ('|', '|'),
+        PyFConf.postProcess = \exp -> do
+          -- TODO: this does not throw an error at compilation time if the json does not parse
+          [|
+            case Json.eitherDecodeStrict' @Json.Value $ textToBytesUtf8 $ stringToText $(exp) of
+              Left err -> error err
+              Right a -> a
+            |]
+      }
diff --git a/users/Profpatsch/mailbox-org/MailboxOrg.hs b/users/Profpatsch/mailbox-org/MailboxOrg.hs
new file mode 100644
index 000000000000..5e062979543b
--- /dev/null
+++ b/users/Profpatsch/mailbox-org/MailboxOrg.hs
@@ -0,0 +1,527 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE NoFieldSelectors #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Main where
+
+import Aeson (parseErrorTree)
+import AesonQQ (aesonQQ)
+import ArglibNetencode
+import Control.Exception (try)
+import Control.Monad (replicateM)
+import Data.Aeson qualified as Json
+import Data.Aeson.BetterErrors qualified as Json
+import Data.Aeson.KeyMap qualified as KeyMap
+import Data.ByteString qualified as ByteString
+import Data.ByteString.Lazy qualified as Lazy
+import Data.Char qualified as Char
+import Data.Error.Tree
+import Data.Functor.Compose
+import Data.List qualified as List
+import Data.Map.Strict qualified as Map
+import Data.Text qualified as Text
+import ExecHelpers
+import GHC.Records (HasField (..))
+import Label
+import MyPrelude
+import Netencode qualified
+import Netencode.Parse qualified as NetParse
+import Network.HTTP.Conduit qualified as Client
+import Network.HTTP.Simple qualified as Client
+import Pretty
+import System.Directory qualified as File
+import System.Environment qualified as Env
+import System.Exit (ExitCode (ExitFailure, ExitSuccess))
+import System.Exit qualified as Exit
+import System.FilePath ((</>))
+import System.Process.Typed qualified as Process
+import System.Random qualified as Random
+import System.Random.Stateful qualified as Random
+import Prelude hiding (log)
+
+secret :: Tools -> IO (T2 "email" ByteString "password" ByteString)
+secret tools = do
+  T2
+    (label @"email" "mail@profpatsch.de")
+    <$> (label @"password" <$> fromPass "email/mailbox.org")
+  where
+    fromPass name =
+      tools.pass & runToolExpect0 [name]
+
+progName :: CurrentProgramName
+progName = "mailbox-org"
+
+log :: Error -> IO ()
+log err = do
+  putStderrLn (errorContext progName.unCurrentProgramName err & prettyError)
+
+data Tools = Tools
+  { pass :: Tool
+  }
+  deriving stock (Show)
+
+newtype Tool = Tool {unTool :: FilePath}
+  deriving stock (Show)
+
+parseTools :: Applicative m => (Text -> m (Either Error Tool)) -> m (Either ErrorTree Tools)
+parseTools getTool = do
+  let parser =
+        ( do
+            pass <- get "pass"
+            pure Tools {..}
+        )
+  parser & finalize
+  where
+    get name = name & getTool <&> eitherToListValidation & Compose
+    finalize p =
+      p.getCompose
+        <&> first (errorTree "Error reading tools")
+        <&> validationToEither
+
+main :: IO ()
+main =
+  arglibNetencode progName Nothing
+    >>= parseToolsArglib
+    >>= secret
+    >>= run applyFilters
+
+run ::
+  ( HasField "email" dat ByteString,
+    HasField "password" dat ByteString
+  ) =>
+  (Session -> IO ()) ->
+  dat ->
+  IO ()
+run act loginData = do
+  session <- login loginData
+  act session
+
+listFilterConfig :: Session -> IO ()
+listFilterConfig session = do
+  mailfilter
+    session
+    "config"
+    mempty
+    (Json.key "data" Json.asObject)
+    ()
+    >>= printPretty
+
+applyFilterRule ::
+  ( HasField "folderId" dat Text,
+    HasField "rulename" dat Text
+  ) =>
+  dat ->
+  Session ->
+  IO ()
+applyFilterRule dat session = do
+  mailfilter
+    session
+    "apply"
+    ( T2
+        (label @"extraQueryParams" [("folderId", Just (dat.folderId & textToBytesUtf8))])
+        mempty
+    )
+    (Json.key "data" Json.asArray >> pure ())
+    (Json.Object mempty)
+
+data FilterRule = FilterRule
+  { actioncmds :: NonEmpty Json.Object,
+    test :: NonEmpty Json.Object
+  }
+
+data MailfilterList = MailfilterList
+  { id_ :: Json.Value,
+    rulename :: Text
+  }
+  deriving stock (Show, Eq)
+
+simpleRule ::
+  ( HasField "rulename" r Text,
+    HasField "id" r Natural,
+    HasField "emailContains" r Text,
+    HasField "subjectStartsWith" r Text
+  ) =>
+  r ->
+  Json.Value
+simpleRule dat = do
+  [aesonQQ|{
+    "id": |dat.id & enc @Natural|,
+    "position": 3,
+    "rulename": |dat.rulename & enc @Text|,
+    "active": true,
+    "flags": [],
+    "test": {
+      "id": "allof",
+      "tests": [
+        {
+          "id": "from",
+          "comparison": "contains",
+          "values": [
+            |dat.emailContains & enc @Text|
+          ]
+        },
+        {
+          "id": "subject",
+          "comparison": "startswith",
+          "values": [
+            |dat.subjectStartsWith & enc @Text|
+          ]
+        }
+      ]
+    },
+    "actioncmds": [
+      {
+        "id": "move",
+        "into": "default0/Archive"
+      },
+      {
+        "id": "stop"
+      }
+    ]
+  }|]
+  where
+    enc :: forall a. Json.ToJSON a => a -> Lazy.ByteString
+    enc val = val & Json.toJSON & Json.encode
+
+applyFilters :: Session -> IO ()
+applyFilters session = do
+  filters <-
+    mailfilter
+      session
+      "list"
+      mempty
+      ( Json.key "data" $ do
+          ( Json.eachInArray $ asDat @"mailfilter" $ do
+              id_ <- Json.key "id" Json.asValue
+              rulename <- Json.key "rulename" Json.asText
+              pure MailfilterList {..}
+            )
+            <&> mapFromListOn (\dat -> getLabel @"rulename" dat.parsed)
+      )
+      ([] :: [()])
+  let goal = Map.fromList [(label @"rulename" "another", 32), (label @"rulename" "xyz", 23)]
+  let actions = declarativeUpdate goal filters
+  log [fmt|To * create: {actions.toCreate & Map.keys & show}, * update: {actions.toUpdate & Map.keys & show}, * delete: {actions.toDelete & Map.keys & show}|]
+  where
+    -- filters
+    --   & Map.elems
+    --   & traverse_
+    --     ( updateIfDifferent
+    --         session
+    --         ( \el ->
+    --             pure $
+    --               el.original.mailfilter
+    --                 & KeyMap.insert "active" (Json.Bool False)
+    --         )
+    --         (pure ())
+    --     )
+
+    mapFromListOn :: Ord k => (a -> k) -> [a] -> Map k a
+    mapFromListOn on xs = xs <&> (\x -> (on x, x)) & Map.fromList
+    updateIfDifferent ::
+      forall label parsed.
+      ( HasField "id_" parsed Json.Value,
+        HasField "rulename" parsed Text
+      ) =>
+      Session ->
+      (Dat label Json.Object parsed -> IO Json.Object) ->
+      Json.Parse Error () ->
+      Dat label Json.Object parsed ->
+      IO ()
+    updateIfDifferent session switcheroo parser dat = do
+      new <- switcheroo dat
+      if new /= getField @label dat.original
+        then do
+          log [fmt|Updating filter "{dat.parsed.rulename}" (id {dat.parsed.id_ & show @Json.Value})|]
+          mailfilter
+            session
+            "update"
+            mempty
+            parser
+            new
+        else do
+          log [fmt|Skipping updating filter "{dat.parsed.rulename}" (id {dat.parsed.id_ & show @Json.Value}) because nothing changed.|]
+
+-- | https://oxpedia.org/wiki/index.php?title=HTTP_API_MailFilter
+mailfilter ::
+  ( Json.ToJSON a,
+    Show b
+  ) =>
+  Session ->
+  ByteString ->
+  T2
+    "extraQueryParams"
+    Client.Query
+    "httpMethod"
+    (Maybe ByteString) ->
+  Json.Parse Error b ->
+  a ->
+  IO b
+mailfilter session action opts parser body = do
+  req <-
+    Client.parseRequest "https://office.mailbox.org/appsuite/api/mailfilter/v2"
+      <&> Client.setQueryString
+        ( [ ("action", Just action),
+            ("colums", Just "1")
+          ]
+            <> opts.extraQueryParams
+        )
+      <&> Client.setRequestMethod (opts.httpMethod & fromMaybe "PUT")
+      <&> Client.setRequestBodyJSON body
+      <&> addSession session
+  req
+    & httpJSON [fmt|Cannot parse result for {req & prettyRequestShort}|] parser
+    >>= okOrDie
+    -- >>= (\resp -> printPretty resp >> pure resp)
+    <&> Client.responseBody
+  where
+    prettyRequestShort :: Client.Request -> Text
+    prettyRequestShort req = [fmt|request {req & Client.method}: {req & Client.host}{req & Client.path}{req & Client.queryString}|]
+
+-- | Given a goal and the actual state, return which elements to delete, update and create.
+declarativeUpdate ::
+  Ord k =>
+  -- | goal map
+  Map k a ->
+  -- | actual map
+  Map k b ->
+  T3
+    "toCreate"
+    (Map k a)
+    "toDelete"
+    (Map k b)
+    "toUpdate"
+    (Map k a)
+declarativeUpdate goal actual =
+  T3
+    (label @"toCreate" $ goal `Map.difference` actual)
+    (label @"toDelete" $ actual `Map.difference` goal)
+    (label @"toUpdate" $ goal `Map.intersection` actual)
+
+newtype Session = Session Client.CookieJar
+
+httpJSON ::
+  Error ->
+  Json.Parse Error b ->
+  Client.Request ->
+  IO (Client.Response b)
+httpJSON errMsg parser req = do
+  req
+    & Client.httpJSON @_ @Json.Value
+    >>= traverse
+      ( \val -> do
+          case val of
+            Json.Object obj
+              | "error" `KeyMap.member` obj
+                  && "error_desc" `KeyMap.member` obj -> do
+                  printPretty obj
+                  diePanic' "Server returned above inline error"
+            _ -> pure ()
+          val & Json.parseValue parser & \case
+            Left errs ->
+              errs
+                & parseErrorTree errMsg
+                & diePanic'
+            Right a -> pure a
+      )
+
+data Dat label orig parsed = Dat
+  { original :: Label label orig,
+    parsed :: parsed
+  }
+  deriving stock (Show, Eq)
+
+asDat ::
+  forall label err m a.
+  Monad m =>
+  Json.ParseT err m a ->
+  Json.ParseT err m (Dat label Json.Object a)
+asDat parser = do
+  original <- label @label <$> Json.asObject
+  parsed <- parser
+  pure Dat {..}
+
+addSession :: Session -> Client.Request -> Client.Request
+addSession (Session jar) req = do
+  let sessionId =
+        jar
+          & Client.destroyCookieJar
+          & List.find (\c -> "open-xchange-session-" `ByteString.isPrefixOf` c.cookie_name)
+          & annotate "The cookie jar did not contain an open-exchange-session-*"
+          & unwrapError
+          & (.cookie_value)
+
+  let req' = req & Client.addToRequestQueryString [("session", Just sessionId)]
+  req' {Client.cookieJar = Just jar}
+
+-- | Log into the mailbox.org service, and return the session secret cookies.
+login :: (HasField "email" dat ByteString, HasField "password" dat ByteString) => dat -> IO Session
+login dat = do
+  rnd <- randomString
+  req <-
+    Client.parseRequest "https://office.mailbox.org/ajax/login"
+      <&> Client.setQueryString
+        [ ("action", Just "formlogin"),
+          ("authId", Just $ ("mbo-" <> rnd) & stringToText & textToBytesUtf8)
+        ]
+      <&> Client.urlEncodedBody
+        [ ("version", "Form+Login"),
+          ("autologin", "true"),
+          ("client", "open-xchange-appsuite"),
+          ("uiWebPath", "/appsuite/"),
+          ("login", dat.email),
+          ("password", dat.password)
+        ]
+  Client.httpNoBody req
+    >>= okOrDie
+    <&> Client.responseCookieJar
+    <&> Session
+  where
+    -- For some reason they want the client to pass a random string
+    -- which is used for the session?‽!?
+    randomString = do
+      gen <- Random.newIOGenM =<< Random.newStdGen
+      let chars = ['a' .. 'z'] <> ['A' .. 'Z'] <> ['0' .. '9']
+      let len = 11
+      Random.uniformRM (0, List.length chars - 1) gen
+        & replicateM len
+        <&> map (\index -> chars !! index)
+
+okOrDie :: Show a => Client.Response a -> IO (Client.Response a)
+okOrDie resp =
+  case resp & Client.getResponseStatusCode of
+    200 -> pure resp
+    _ -> do
+      printPretty resp
+      diePanic' "non-200 result"
+
+diePanic' :: ErrorTree -> IO a
+diePanic' errs = errs & prettyErrorTree & diePanic progName
+
+-- | Parse the tools from the given arglib input, and check that the executables exist
+parseToolsArglib :: Netencode.T -> IO Tools
+parseToolsArglib t = do
+  let oneTool name =
+        NetParse.asText
+          <&> textToString
+          <&> ( \path ->
+                  path
+                    & File.getPermissions
+                    <&> File.executable
+                    <&> ( \case
+                            False -> Left $ [fmt|Tool "{name}" is not an executable|]
+                            True -> Right (Tool path)
+                        )
+              )
+  let allTools =
+        parseTools (\name -> Compose $ NetParse.key name >>> oneTool name)
+          & getCompose
+  t
+    & NetParse.runParse
+      "test"
+      -- TODO: a proper ParseT for netencode values
+      ( NetParse.asRecord
+          >>> NetParse.key "BINS"
+          >>> NetParse.asRecord
+          >>> allTools
+      )
+    & orDo diePanic'
+    & join @IO
+    >>= orDo (\errs -> errs & diePanic')
+
+-- | Just assume the tools exist by name in the environment.
+parseToolsToolname :: IO Tools
+parseToolsToolname =
+  parseTools
+    ( \name ->
+        checkInPath name <&> \case
+          False -> Left [fmt|"Cannot find "{name}" in PATH|]
+          True -> Right $ Tool (name & textToString)
+    )
+    >>= orDo diePanic'
+
+checkInPath :: Text -> IO Bool
+checkInPath name = do
+  Env.lookupEnv "PATH"
+    <&> annotate "No PATH set"
+    >>= orDo diePanic'
+    <&> stringToText
+    <&> Text.split (== ':')
+    <&> filter (/= "")
+    >>= traverse
+      ( \p ->
+          File.getPermissions ((textToString p) </> (textToString name))
+            <&> File.executable
+            & try @IOError
+            >>= \case
+              Left _ioError -> pure False
+              Right isExe -> pure isExe
+      )
+    <&> or
+
+orDo :: Applicative f => (t -> f a) -> Either t a -> f a
+orDo f = \case
+  Left e -> f e
+  Right a -> pure a
+
+runTool :: [Text] -> Tool -> IO (Exit.ExitCode, ByteString)
+runTool args tool = do
+  let bashArgs = prettyArgsForBash ((tool.unTool & stringToText) : args)
+  log [fmt|Running: $ {bashArgs}|]
+  Process.proc
+    tool.unTool
+    (args <&> textToString)
+    & Process.readProcessStdout
+    <&> second toStrictBytes
+    <&> second stripWhitespaceFromEnd
+
+-- | Like `runCommandExpect0`, run the given tool, given a tool accessor.
+runToolExpect0 :: [Text] -> Tool -> IO ByteString
+runToolExpect0 args tool =
+  tool & runTool args >>= \(ex, stdout) -> do
+    checkStatus0 tool.unTool ex
+    pure stdout
+
+-- | Check whether a command exited 0 or crash.
+checkStatus0 :: FilePath -> ExitCode -> IO ()
+checkStatus0 executable = \case
+  ExitSuccess -> pure ()
+  ExitFailure status -> do
+    diePanic' [fmt|Command `{executable}` did not exit with status 0 (success), but status {status}|]
+
+stripWhitespaceFromEnd :: ByteString -> ByteString
+stripWhitespaceFromEnd = ByteString.reverse . ByteString.dropWhile (\w -> w == charToWordUnsafe '\n') . ByteString.reverse
+
+-- | Pretty print a command line in a way that can be copied to bash.
+prettyArgsForBash :: [Text] -> Text
+prettyArgsForBash = Text.intercalate " " . map simpleBashEscape
+
+-- | Simple escaping for bash words. If they contain anything that’s not ascii chars
+-- and a bunch of often-used special characters, put the word in single quotes.
+simpleBashEscape :: Text -> Text
+simpleBashEscape t = do
+  case Text.find (not . isSimple) t of
+    Just _ -> escapeSingleQuote t
+    Nothing -> t
+  where
+    -- any word that is just ascii characters is simple (no spaces or control characters)
+    -- or contains a few often-used characters like - or .
+    isSimple c =
+      Char.isAsciiLower c
+        || Char.isAsciiUpper c
+        || Char.isDigit c
+        -- These are benign, bash will not interpret them as special characters.
+        || List.elem c ['-', '.', ':', '/']
+    -- Put the word in single quotes
+    -- If there is a single quote in the word,
+    -- close the single quoted word, add a single quote, open the word again
+    escapeSingleQuote t' = "'" <> Text.replace "'" "'\\''" t' <> "'"
diff --git a/users/Profpatsch/mailbox-org/README.md b/users/Profpatsch/mailbox-org/README.md
new file mode 100644
index 000000000000..b84e7b59c130
--- /dev/null
+++ b/users/Profpatsch/mailbox-org/README.md
@@ -0,0 +1,7 @@
+# mailbox-org
+
+Interfacing with the API of [https://mailbox.org/]().
+
+They use [open-xchange](https://www.open-xchange.com/resources/oxpedia) as their App Suite, so we have to work with/reverse engineer their weird API.
+
+Intended so I have a way of uploading Sieve rules into their system semi-automatically.
diff --git a/users/Profpatsch/mailbox-org/default.nix b/users/Profpatsch/mailbox-org/default.nix
new file mode 100644
index 000000000000..cd419047223f
--- /dev/null
+++ b/users/Profpatsch/mailbox-org/default.nix
@@ -0,0 +1,35 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  mailbox-org = pkgs.haskellPackages.mkDerivation {
+    pname = "mailbox-org";
+    version = "0.1.0";
+
+    src = depot.users.Profpatsch.exactSource ./. [
+      ./mailbox-org.cabal
+      ./AesonQQ.hs
+      ./MailboxOrg.hs
+    ];
+
+    libraryHaskellDepends = [
+      depot.users.Profpatsch.my-prelude
+      depot.users.Profpatsch.execline.exec-helpers-hs
+      depot.users.Profpatsch.arglib.netencode.haskell
+      pkgs.haskellPackages.aeson
+      pkgs.haskellPackages.http-conduit
+      pkgs.haskellPackages.aeson-better-errors
+    ];
+
+    isLibrary = false;
+    isExecutable = true;
+    license = lib.licenses.mit;
+  };
+
+
+in
+lib.pipe mailbox-org [
+  (x: (depot.nix.getBins x [ "mailbox-org" ]).mailbox-org)
+  (depot.users.Profpatsch.arglib.netencode.with-args "mailbox-org" {
+    BINS = depot.nix.getBins pkgs.dovecot_pigeonhole [ "sieve-test" ];
+  })
+]
diff --git a/users/Profpatsch/mailbox-org/mailbox-org.cabal b/users/Profpatsch/mailbox-org/mailbox-org.cabal
new file mode 100644
index 000000000000..fe65da409df6
--- /dev/null
+++ b/users/Profpatsch/mailbox-org/mailbox-org.cabal
@@ -0,0 +1,38 @@
+cabal-version:      2.4
+name:               mailbox-org
+version:            0.1.0.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+executable mailbox-org
+    main-is: MailboxOrg.hs
+
+    build-depends:
+        base >=4.15 && <5,
+        my-prelude,
+        exec-helpers,
+        netencode,
+        text,
+        semigroupoids,
+        nonempty-containers,
+        data-fix,
+        selective,
+        directory,
+        mtl,
+        filepath,
+        arglib-netencode,
+        random,
+        http-conduit,
+        http-client,
+        aeson,
+        aeson-better-errors,
+        bytestring,
+        PyF,
+        typed-process,
+        process,
+        containers,
+
+    default-language: Haskell2010
+
+    default-extensions:
+        GHC2021
diff --git a/users/Profpatsch/my-prelude/Aeson.hs b/users/Profpatsch/my-prelude/Aeson.hs
new file mode 100644
index 000000000000..ad095e1b43a7
--- /dev/null
+++ b/users/Profpatsch/my-prelude/Aeson.hs
@@ -0,0 +1,188 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Aeson where
+
+import Data.Aeson (Encoding, FromJSON (parseJSON), GFromJSON, GToEncoding, GToJSON, Options (fieldLabelModifier), ToJSON (toEncoding, toJSON), Value (..), Zero, defaultOptions, genericParseJSON, genericToEncoding, genericToJSON, withObject)
+import Data.Aeson.BetterErrors qualified as Json
+import Data.Aeson.Encoding qualified as Enc
+import Data.Aeson.Key qualified as Key
+import Data.Aeson.KeyMap qualified as KeyMap
+import Data.Char qualified
+import Data.Error.Tree
+import Data.Foldable qualified as Foldable
+import Data.Int (Int64)
+import Data.List (isPrefixOf)
+import Data.List qualified as List
+import Data.Map.Strict qualified as Map
+import Data.Maybe (catMaybes)
+import Data.String (IsString (fromString))
+import Data.Text.Lazy qualified as Lazy
+import Data.Vector qualified as Vector
+import GHC.Generics (Generic (Rep))
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+import Label
+import MyPrelude
+import Test.Hspec (describe, it, shouldBe)
+import Test.Hspec qualified as Hspec
+
+-- | Convert a 'Json.ParseError' to a corresponding 'ErrorTree'
+parseErrorTree :: Error -> Json.ParseError Error -> ErrorTree
+parseErrorTree contextMsg errs =
+  errs
+    & Json.displayError prettyError
+    <&> newError
+    & nonEmpty
+    & \case
+      Nothing -> singleError contextMsg
+      Just errs' -> errorTree contextMsg errs'
+
+-- | Parse a key from the object, à la 'Json.key', return a labelled value.
+--
+-- We don’t provide a version that infers the json object key,
+-- since that conflates internal naming with the external API, which is dangerous.
+--
+-- @@
+-- do
+--   txt <- keyLabel @"myLabel" "jsonKeyName" Json.asText
+--   pure (txt :: Label "myLabel" Text)
+-- @@
+keyLabel ::
+  forall label err m a.
+  Monad m =>
+  Text ->
+  Json.ParseT err m a ->
+  Json.ParseT err m (Label label a)
+keyLabel = do
+  keyLabel' (Proxy @label)
+
+-- | Parse a key from the object, à la 'Json.key', return a labelled value.
+-- Version of 'keyLabel' that requires a proxy.
+--
+-- @@
+-- do
+--   txt <- keyLabel' (Proxy @"myLabel") "jsonKeyName" Json.asText
+--   pure (txt :: Label "myLabel" Text)
+-- @@
+keyLabel' ::
+  forall label err m a.
+  Monad m =>
+  Proxy label ->
+  Text ->
+  Json.ParseT err m a ->
+  Json.ParseT err m (Label label a)
+keyLabel' Proxy key parser = label @label <$> Json.key key parser
+
+-- | Parse an optional key from the object, à la 'Json.keyMay', return a labelled value.
+--
+-- We don’t provide a version that infers the json object key,
+-- since that conflates internal naming with the external API, which is dangerous.
+--
+-- @@
+-- do
+--   txt <- keyLabelMay @"myLabel" "jsonKeyName" Json.asText
+--   pure (txt :: Label "myLabel" (Maybe Text))
+-- @@
+keyLabelMay ::
+  forall label err m a.
+  Monad m =>
+  Text ->
+  Json.ParseT err m a ->
+  Json.ParseT err m (Label label (Maybe a))
+keyLabelMay = do
+  keyLabelMay' (Proxy @label)
+
+-- | Parse an optional key from the object, à la 'Json.keyMay', return a labelled value.
+-- Version of 'keyLabelMay' that requires a proxy.
+--
+-- @@
+-- do
+--   txt <- keyLabelMay' (Proxy @"myLabel") "jsonKeyName" Json.asText
+--   pure (txt :: Label "myLabel" (Maybe Text))
+-- @@
+keyLabelMay' ::
+  forall label err m a.
+  Monad m =>
+  Proxy label ->
+  Text ->
+  Json.ParseT err m a ->
+  Json.ParseT err m (Label label (Maybe a))
+keyLabelMay' Proxy key parser = label @label <$> Json.keyMay key parser
+
+-- | Like 'Json.key', but allows a list of keys that are tried in order.
+--
+-- This is intended for renaming keys in an object.
+-- The first key is the most up-to-date version of a key, the others are for backward-compatibility.
+--
+-- If a key (new or old) exists, the inner parser will always be executed for that key.
+keyRenamed :: Monad m => NonEmpty Text -> Json.ParseT err m a -> Json.ParseT err m a
+keyRenamed (newKey :| oldKeys) inner =
+  keyRenamedTryOldKeys oldKeys inner >>= \case
+    Nothing -> Json.key newKey inner
+    Just parse -> parse
+
+-- | Like 'Json.keyMay', but allows a list of keys that are tried in order.
+--
+-- This is intended for renaming keys in an object.
+-- The first key is the most up-to-date version of a key, the others are for backward-compatibility.
+--
+-- If a key (new or old) exists, the inner parser will always be executed for that key.
+keyRenamedMay :: Monad m => NonEmpty Text -> Json.ParseT err m a -> Json.ParseT err m (Maybe a)
+keyRenamedMay (newKey :| oldKeys) inner =
+  keyRenamedTryOldKeys oldKeys inner >>= \case
+    Nothing -> Json.keyMay newKey inner
+    Just parse -> Just <$> parse
+
+-- | Helper function for 'keyRenamed' and 'keyRenamedMay' that returns the parser for the first old key that exists, if any.
+keyRenamedTryOldKeys :: Monad m => [Text] -> Json.ParseT err m a -> Json.ParseT err m (Maybe (Json.ParseT err m a))
+keyRenamedTryOldKeys oldKeys inner = do
+  oldKeys & traverse tryOld <&> catMaybes <&> nonEmpty <&> \case
+    Nothing -> Nothing
+    Just (old :| _moreOld) -> Just old
+  where
+    tryOld key =
+      Json.keyMay key (pure ()) <&> \case
+        Just () -> Just $ Json.key key inner
+        Nothing -> Nothing
+
+test_keyRenamed :: Hspec.Spec
+test_keyRenamed = do
+  describe "keyRenamed" $ do
+    let parser = keyRenamed ("new" :| ["old"]) Json.asText
+    let p = Json.parseValue @() parser
+    it "accepts the new key and the old key" $ do
+      p (Object (KeyMap.singleton "new" (String "text")))
+        `shouldBe` (Right "text")
+      p (Object (KeyMap.singleton "old" (String "text")))
+        `shouldBe` (Right "text")
+    it "fails with the old key in the error if the inner parser is wrong" $ do
+      p (Object (KeyMap.singleton "old" Null))
+        `shouldBe` (Left (Json.BadSchema [Json.ObjectKey "old"] (Json.WrongType Json.TyString Null)))
+    it "fails with the new key in the error if the inner parser is wrong" $ do
+      p (Object (KeyMap.singleton "new" Null))
+        `shouldBe` (Left (Json.BadSchema [Json.ObjectKey "new"] (Json.WrongType Json.TyString Null)))
+    it "fails if the key is missing" $ do
+      p (Object KeyMap.empty)
+        `shouldBe` (Left (Json.BadSchema [] (Json.KeyMissing "new")))
+  describe "keyRenamedMay" $ do
+    let parser = keyRenamedMay ("new" :| ["old"]) Json.asText
+    let p = Json.parseValue @() parser
+    it "accepts the new key and the old key" $ do
+      p (Object (KeyMap.singleton "new" (String "text")))
+        `shouldBe` (Right (Just "text"))
+      p (Object (KeyMap.singleton "old" (String "text")))
+        `shouldBe` (Right (Just "text"))
+    it "allows the old and new key to be missing" $ do
+      p (Object KeyMap.empty)
+        `shouldBe` (Right Nothing)
+
+-- | Create a json array from a list of json values.
+jsonArray :: [Value] -> Value
+jsonArray xs = xs & Vector.fromList & Array
diff --git a/users/Profpatsch/my-prelude/Data/Error/Tree.hs b/users/Profpatsch/my-prelude/Data/Error/Tree.hs
new file mode 100644
index 000000000000..e8e45e704882
--- /dev/null
+++ b/users/Profpatsch/my-prelude/Data/Error/Tree.hs
@@ -0,0 +1,113 @@
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+
+module Data.Error.Tree where
+
+import Data.String (IsString (..))
+import Data.Tree qualified as Tree
+import MyPrelude
+
+-- | A tree of 'Error's, with a single root 'Error' and 0..n nested 'ErrorTree's.
+--
+-- @@
+-- top error
+-- |
+-- |-- error 1
+-- | |
+-- |  -- error 1.1
+-- |
+-- |-- error 2
+-- @@
+newtype ErrorTree = ErrorTree {unErrorTree :: (Tree.Tree Error)}
+  deriving stock (Show)
+
+instance IsString ErrorTree where
+  fromString = singleError . fromString
+
+-- deriving newtype (Ord) -- TODO: Add this instance with containers-0.6.5
+
+-- | Turn a single 'Error' into an 'ErrorTree', a leaf.
+singleError :: Error -> ErrorTree
+singleError e = ErrorTree $ Tree.Node e []
+
+-- | Take a list of errors & create a new 'ErrorTree' with the given 'Error' as the root.
+errorTree :: Error -> NonEmpty Error -> ErrorTree
+errorTree topLevelErr nestedErrs =
+  ErrorTree
+    ( Tree.Node
+        topLevelErr
+        (nestedErrs <&> (\e -> Tree.Node e []) & toList)
+    )
+
+-- | Attach more context to the root 'Error' of the 'ErrorTree', via 'errorContext'.
+errorTreeContext :: Text -> ErrorTree -> ErrorTree
+errorTreeContext context (ErrorTree tree) =
+  ErrorTree $
+    tree
+      { Tree.rootLabel = tree.rootLabel & errorContext context
+      }
+
+-- | Nest the given 'Error' around the ErrorTree
+--
+-- @@
+-- top level error
+-- |
+-- -- nestedError
+--   |
+--   -- error 1
+--   |
+--   -- error 2
+-- @@
+nestedError ::
+  Error -> -- top level
+  ErrorTree -> -- nested
+  ErrorTree
+nestedError topLevelErr nestedErr =
+  ErrorTree $
+    Tree.Node
+      { Tree.rootLabel = topLevelErr,
+        Tree.subForest = [nestedErr.unErrorTree]
+      }
+
+-- | Nest the given 'Error' around the list of 'ErrorTree's.
+--
+-- @@
+-- top level error
+-- |
+-- |- nestedError1
+-- | |
+-- | -- error 1
+-- | |
+-- | -- error 2
+-- |
+-- |- nestedError 2
+-- @@
+nestedMultiError ::
+  Error -> -- top level
+  NonEmpty ErrorTree -> -- nested
+  ErrorTree
+nestedMultiError topLevelErr nestedErrs =
+  ErrorTree $
+    Tree.Node
+      { Tree.rootLabel = topLevelErr,
+        Tree.subForest = nestedErrs & toList <&> (.unErrorTree)
+      }
+
+prettyErrorTree :: ErrorTree -> Text
+prettyErrorTree (ErrorTree tree) =
+  tree
+    <&> prettyError
+    <&> textToString
+    & Tree.drawTree
+    & stringToText
+
+prettyErrorTrees :: NonEmpty ErrorTree -> Text
+prettyErrorTrees forest =
+  forest
+    <&> (.unErrorTree)
+    <&> fmap prettyError
+    <&> fmap textToString
+    & toList
+    & Tree.drawForest
+    & stringToText
diff --git a/users/Profpatsch/my-prelude/Label.hs b/users/Profpatsch/my-prelude/Label.hs
new file mode 100644
index 000000000000..01b49353b987
--- /dev/null
+++ b/users/Profpatsch/my-prelude/Label.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE KindSignatures #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Label
+  ( Label,
+    label,
+    label',
+    getLabel,
+    T2 (..),
+    T3 (..),
+  )
+where
+
+import Data.Data (Proxy (..))
+import Data.Function ((&))
+import Data.Typeable (Typeable)
+import GHC.Records (HasField (..))
+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
+
+-- | A labelled value.
+--
+-- Use 'label'/'label'' to construct,
+-- then use dot-syntax to get the inner value.
+newtype Label (label :: Symbol) value = Label value
+  deriving stock (Eq, Ord)
+  deriving newtype (Typeable, Semigroup, Monoid)
+
+instance (KnownSymbol label, Show value) => Show (Label label value) where
+  showsPrec d (Label val) =
+    showParen (d > 10) $
+      showString "Label @"
+        . showsPrec 11 (symbolVal (Proxy @label))
+        . showString " "
+        . showsPrec 11 val
+
+-- | Attach a label to a value; should be used with a type application to name the label.
+--
+-- @@
+-- let f = label @"foo" 'f' :: Label "foo" Char
+-- in f.foo :: Char
+-- @@
+--
+-- Use dot-syntax to get the labelled value.
+label :: forall label value. value -> Label label value
+label value = Label value
+
+-- | Attach a label to a value; Pass it a proxy with the label name in the argument type.
+-- This is intended for passing through the label value;
+-- you can also use 'label'.
+--
+--
+-- @@
+-- let f = label' (Proxy @"foo") 'f' :: Label "foo" Char
+-- in f.foo :: Char
+-- @@
+--
+-- Use dot-syntax to get the labelled value.
+label' :: forall label value. (Proxy label) -> value -> Label label value
+label' Proxy value = Label value
+
+-- | Fetches the labelled value.
+instance HasField label (Label label value) value where
+  getField :: (Label label value) -> value
+  getField (Label value) = value
+
+-- | Fetch a value from a record, like 'getField', but also keep it wrapped by its label.
+getLabel :: forall label record a. HasField label record a => record -> Label label a
+getLabel rec = rec & getField @label & label @label
+
+-- | A named 2-element tuple. Since the elements are named, you can access them with `.`.
+--
+-- @@
+-- let t2 = T2 (label @"myfield" 'c') (label @"otherfield" True) :: T2 "myfield" Char "otherfield" Bool
+-- in (
+--   t2.myfield :: Char,
+--   t2.otherfield :: Bool
+-- )
+-- @@
+data T2 (l1 :: Symbol) t1 (l2 :: Symbol) t2 = T2 (Label l1 t1) (Label l2 t2)
+
+-- | Access the first field by label
+instance HasField l1 (T2 l1 t1 l2 t2) t1 where
+  getField (T2 t1 _) = getField @l1 t1
+
+-- | Access the second field by label
+instance HasField l2 (T2 l1 t1 l2 t2) t2 where
+  getField (T2 _ t2) = getField @l2 t2
+
+instance (Semigroup t1, Semigroup t2) => Semigroup (T2 l1 t1 l2 t2) where
+  T2 t1 t2 <> T2 t1' t2' = T2 (t1 <> t1') (t2 <> t2')
+
+instance (Monoid t1, Monoid t2) => Monoid (T2 l1 t1 l2 t2) where
+  mempty = T2 mempty mempty
+
+-- | A named 3-element tuple. Since the elements are named, you can access them with `.`. See 'T2' for an example.
+data T3 (l1 :: Symbol) t1 (l2 :: Symbol) t2 (l3 :: Symbol) t3 = T3 (Label l1 t1) (Label l2 t2) (Label l3 t3)
+
+-- | Access the first field by label
+instance HasField l1 (T3 l1 t1 l2 t2 l3 t3) t1 where
+  getField (T3 t1 _ _) = getField @l1 t1
+
+-- | Access the second field by label
+instance HasField l2 (T3 l1 t1 l2 t2 l3 t3) t2 where
+  getField (T3 _ t2 _) = getField @l2 t2
+
+-- | Access the third field by label
+instance HasField l3 (T3 l1 t1 l2 t2 l3 t3) t3 where
+  getField (T3 _ _ t3) = getField @l3 t3
+
+instance (Semigroup t1, Semigroup t2, Semigroup t3) => Semigroup (T3 l1 t1 l2 t2 l3 t3) where
+  T3 t1 t2 t3 <> T3 t1' t2' t3' = T3 (t1 <> t1') (t2 <> t2') (t3 <> t3')
+
+instance (Monoid t1, Monoid t2, Monoid t3) => Monoid (T3 l1 t1 l2 t2 l3 t3) where
+  mempty = T3 mempty mempty mempty
diff --git a/users/Profpatsch/my-prelude/MyPrelude.hs b/users/Profpatsch/my-prelude/MyPrelude.hs
new file mode 100644
index 000000000000..1be248d091a9
--- /dev/null
+++ b/users/Profpatsch/my-prelude/MyPrelude.hs
@@ -0,0 +1,587 @@
+{-# LANGUAGE ImplicitParams #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MagicHash #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PolyKinds #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# OPTIONS_GHC -fexpose-all-unfoldings #-}
+
+module MyPrelude
+  ( -- * Text conversions
+    Text,
+    ByteString,
+    Word8,
+    fmt,
+    textToString,
+    stringToText,
+    showToText,
+    textToBytesUtf8,
+    textToBytesUtf8Lazy,
+    bytesToTextUtf8,
+    bytesToTextUtf8Lazy,
+    bytesToTextUtf8Lenient,
+    bytesToTextUtf8LenientLazy,
+    bytesToTextUtf8Unsafe,
+    bytesToTextUtf8UnsafeLazy,
+    toStrict,
+    toLazy,
+    toStrictBytes,
+    toLazyBytes,
+    charToWordUnsafe,
+
+    -- * IO
+    putStrLn,
+    putStderrLn,
+    exitWithMessage,
+
+    -- * WIP code
+    todo,
+
+    -- * Records
+    HasField,
+
+    -- * Control flow
+    (&),
+    (<&>),
+    (<|>),
+    foldMap1,
+    foldMap',
+    join,
+    when,
+    unless,
+    guard,
+    ExceptT (..),
+    runExceptT,
+    MonadThrow,
+    throwM,
+    MonadIO,
+    liftIO,
+    MonadReader,
+    asks,
+    Bifunctor,
+    first,
+    second,
+    bimap,
+    both,
+    foldMap,
+    fold,
+    foldl',
+    fromMaybe,
+    mapMaybe,
+    findMaybe,
+    Traversable,
+    for,
+    for_,
+    traverse,
+    traverse_,
+    traverseFold,
+    traverseFold1,
+    traverseFoldDefault,
+    MonadTrans,
+    lift,
+
+    -- * Data types
+    Coercible,
+    coerce,
+    Proxy (Proxy),
+    Map,
+    annotate,
+    Validation (Success, Failure),
+    failure,
+    successes,
+    failures,
+    eitherToValidation,
+    eitherToListValidation,
+    validationToEither,
+    These (This, That, These),
+    eitherToThese,
+    eitherToListThese,
+    validationToThese,
+    thenThese,
+    thenValidate,
+    NonEmpty ((:|)),
+    singleton,
+    nonEmpty,
+    nonEmptyDef,
+    toList,
+    toNonEmptyDefault,
+    maximum1,
+    minimum1,
+    Generic,
+    Semigroup,
+    sconcat,
+    Monoid,
+    mconcat,
+    ifTrue,
+    ifExists,
+    Void,
+    absurd,
+    Identity (Identity, runIdentity),
+    Natural,
+    intToNatural,
+    Contravariant,
+    contramap,
+    (>$<),
+    (>&<),
+    Profunctor,
+    dimap,
+    lmap,
+    rmap,
+    Semigroupoid,
+    Category,
+    (>>>),
+    (&>>),
+
+    -- * Enum definition
+    inverseFunction,
+    inverseMap,
+
+    -- * Error handling
+    HasCallStack,
+    module Data.Error,
+  )
+where
+
+import Control.Applicative ((<|>))
+import Control.Category (Category, (>>>))
+import Control.Monad (guard, join, unless, when)
+import Control.Monad.Catch (MonadThrow (throwM))
+import Control.Monad.Except
+  ( ExceptT (..),
+    runExceptT,
+  )
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Control.Monad.Identity (Identity (Identity))
+import Control.Monad.Reader (MonadReader, asks)
+import Control.Monad.Trans (MonadTrans (lift))
+import Data.Bifunctor (Bifunctor, bimap, first, second)
+import Data.ByteString
+  ( ByteString,
+  )
+import Data.ByteString.Lazy qualified
+import Data.Char qualified
+import Data.Coerce (Coercible, coerce)
+import Data.Data (Proxy (Proxy))
+import Data.Error
+import Data.Foldable (Foldable (foldMap', toList), fold, foldl', for_, traverse_)
+import Data.Foldable qualified as Foldable
+import Data.Function ((&))
+import Data.Functor ((<&>))
+import Data.Functor.Contravariant (Contravariant (contramap), (>$<))
+import Data.Functor.Identity (Identity (runIdentity))
+import Data.List.NonEmpty (NonEmpty ((:|)), nonEmpty)
+import Data.Map.Strict
+  ( Map,
+  )
+import Data.Map.Strict qualified as Map
+import Data.Maybe (fromMaybe, mapMaybe)
+import Data.Maybe qualified as Maybe
+import Data.Profunctor (Profunctor, dimap, lmap, rmap)
+import Data.Semigroup (Max (Max, getMax), Min (Min, getMin), sconcat)
+import Data.Semigroup.Foldable (Foldable1 (fold1), foldMap1)
+import Data.Semigroup.Traversable (Traversable1)
+import Data.Semigroupoid (Semigroupoid (o))
+import Data.Text
+  ( Text,
+  )
+import Data.Text qualified
+import Data.Text.Encoding qualified
+import Data.Text.Encoding.Error qualified
+import Data.Text.Lazy qualified
+import Data.Text.Lazy.Encoding qualified
+import Data.These (These (That, These, This))
+import Data.Traversable (for)
+import Data.Void (Void, absurd)
+import Data.Word (Word8)
+import GHC.Exception (errorCallWithCallStackException)
+import GHC.Exts (RuntimeRep, TYPE, raise#)
+import GHC.Generics (Generic)
+import GHC.Natural (Natural)
+import GHC.Records (HasField)
+import GHC.Stack (HasCallStack)
+import PyF (fmt)
+import System.Exit qualified
+import System.IO qualified
+import Validation
+  ( Validation (Failure, Success),
+    eitherToValidation,
+    failure,
+    failures,
+    successes,
+    validationToEither,
+  )
+
+-- | Forward-applying 'contramap', like '&'/'$' and '<&>'/'<$>' but for '>$<'.
+(>&<) :: Contravariant f => f b -> (a -> b) -> f a
+(>&<) = flip contramap
+
+infixl 5 >&<
+
+-- | Forward semigroupoid application. The same as '(>>>)', but 'Semigroupoid' is not a superclass of 'Category' (yet).
+--
+-- Specialized examples:
+--
+-- @@
+-- for functions : (a -> b) -> (b -> c) -> (a -> c)
+-- for Folds: Fold a b -> Fold b c -> Fold a c
+-- @@
+(&>>) :: Semigroupoid s => s a b -> s b c -> s a c
+(&>>) = flip Data.Semigroupoid.o
+
+-- like >>>
+infixr 1 &>>
+
+-- | encode a Text to a UTF-8 encoded Bytestring
+textToBytesUtf8 :: Text -> ByteString
+textToBytesUtf8 = Data.Text.Encoding.encodeUtf8
+
+-- | encode a lazy Text to a UTF-8 encoded lazy Bytestring
+textToBytesUtf8Lazy :: Data.Text.Lazy.Text -> Data.ByteString.Lazy.ByteString
+textToBytesUtf8Lazy = Data.Text.Lazy.Encoding.encodeUtf8
+
+bytesToTextUtf8 :: ByteString -> Either Error Text
+bytesToTextUtf8 = first exceptionToError . Data.Text.Encoding.decodeUtf8'
+
+bytesToTextUtf8Lazy :: Data.ByteString.Lazy.ByteString -> Either Error Data.Text.Lazy.Text
+bytesToTextUtf8Lazy = first exceptionToError . Data.Text.Lazy.Encoding.decodeUtf8'
+
+-- | decode a Text from a ByteString that is assumed to be UTF-8 (crash if that is not the case)
+bytesToTextUtf8Unsafe :: ByteString -> Text
+bytesToTextUtf8Unsafe = Data.Text.Encoding.decodeUtf8
+
+-- | decode a Text from a ByteString that is assumed to be UTF-8 (crash if that is not the case)
+bytesToTextUtf8UnsafeLazy :: Data.ByteString.Lazy.ByteString -> Data.Text.Lazy.Text
+bytesToTextUtf8UnsafeLazy = Data.Text.Lazy.Encoding.decodeUtf8
+
+-- | decode a Text from a ByteString that is assumed to be UTF-8,
+-- replace non-UTF-8 characters with the replacment char U+FFFD.
+bytesToTextUtf8Lenient :: Data.ByteString.ByteString -> Data.Text.Text
+bytesToTextUtf8Lenient =
+  Data.Text.Encoding.decodeUtf8With Data.Text.Encoding.Error.lenientDecode
+
+-- | decode a lazy Text from a lazy ByteString that is assumed to be UTF-8,
+-- replace non-UTF-8 characters with the replacment char U+FFFD.
+bytesToTextUtf8LenientLazy :: Data.ByteString.Lazy.ByteString -> Data.Text.Lazy.Text
+bytesToTextUtf8LenientLazy =
+  Data.Text.Lazy.Encoding.decodeUtf8With Data.Text.Encoding.Error.lenientDecode
+
+-- | Make a lazy text strict
+toStrict :: Data.Text.Lazy.Text -> Text
+toStrict = Data.Text.Lazy.toStrict
+
+-- | Make a strict text lazy
+toLazy :: Text -> Data.Text.Lazy.Text
+toLazy = Data.Text.Lazy.fromStrict
+
+toStrictBytes :: Data.ByteString.Lazy.ByteString -> ByteString
+toStrictBytes = Data.ByteString.Lazy.toStrict
+
+toLazyBytes :: ByteString -> Data.ByteString.Lazy.ByteString
+toLazyBytes = Data.ByteString.Lazy.fromStrict
+
+textToString :: Text -> String
+textToString = Data.Text.unpack
+
+stringToText :: String -> Text
+stringToText = Data.Text.pack
+
+showToText :: (Show a) => a -> Text
+showToText = stringToText . show
+
+-- | Unsafe conversion between 'Char' and 'Word8'. This is a no-op and
+-- silently truncates to 8 bits Chars > '\255'. It is provided as
+-- convenience for ByteString construction.
+--
+-- Use if you want to get the 'Word8' representation of a character literal.
+-- Don’t use on arbitrary characters!
+--
+-- >>> charToWordUnsafe ','
+-- 44
+charToWordUnsafe :: Char -> Word8
+charToWordUnsafe = fromIntegral . Data.Char.ord
+{-# INLINE charToWordUnsafe #-}
+
+-- | Single element in a (non-empty) list.
+singleton :: a -> NonEmpty a
+singleton a = a :| []
+
+-- | If the given list is empty, use the given default element and return a non-empty list.
+nonEmptyDef :: a -> [a] -> NonEmpty a
+nonEmptyDef def xs =
+  xs & nonEmpty & \case
+    Nothing -> def :| []
+    Just ne -> ne
+
+-- | Construct a non-empty list, given a default value if the ist list was empty.
+toNonEmptyDefault :: a -> [a] -> NonEmpty a
+toNonEmptyDefault def xs = case xs of
+  [] -> def :| []
+  (x : xs') -> x :| xs'
+
+-- | @O(n)@. Get the maximum element from a non-empty structure.
+maximum1 :: (Foldable1 f, Ord a) => f a -> a
+maximum1 xs = xs & foldMap1 Max & getMax
+
+-- | @O(n)@. Get the minimum element from a non-empty structure.
+minimum1 :: (Foldable1 f, Ord a) => f a -> a
+minimum1 xs = xs & foldMap1 Min & getMin
+
+-- | Annotate a 'Maybe' with an error message and turn it into an 'Either'.
+annotate :: err -> Maybe a -> Either err a
+annotate err = \case
+  Nothing -> Left err
+  Just a -> Right a
+
+-- | Map the same function over both sides of a Bifunctor (e.g. a tuple).
+both :: Bifunctor bi => (a -> b) -> bi a a -> bi b b
+both f = bimap f f
+
+-- | Find the first element for which pred returns `Just a`, and return the `a`.
+--
+-- Example:
+-- @
+-- >>> :set -XTypeApplications
+-- >>> import qualified Text.Read
+--
+-- >>> findMaybe (Text.Read.readMaybe @Int) ["foo"]
+-- Nothing
+-- >>> findMaybe (Text.Read.readMaybe @Int) ["foo", "34.40", "34", "abc"]
+-- Just 34
+findMaybe :: Foldable t => (a -> Maybe b) -> t a -> Maybe b
+findMaybe mPred list =
+  let pred' x = Maybe.isJust $ mPred x
+   in case Foldable.find pred' list of
+        Just a -> mPred a
+        Nothing -> Nothing
+
+-- | Like 'eitherToValidation', but puts the Error side into a NonEmpty list
+-- to make it combine with other validations.
+eitherToListValidation :: Either a c -> Validation (NonEmpty a) c
+eitherToListValidation = first singleton . eitherToValidation
+
+-- | Convert an 'Either' to a 'These'.
+eitherToThese :: Either err a -> These err a
+eitherToThese (Left err) = This err
+eitherToThese (Right a) = That a
+
+-- | Like 'eitherToThese', but puts the Error side into a NonEmpty list
+-- to make it combine with other theses.
+eitherToListThese :: Either err a -> These (NonEmpty err) a
+eitherToListThese (Left e) = This (singleton e)
+eitherToListThese (Right a) = That a
+
+-- | Convert a 'Validation' to a 'These'.
+validationToThese :: Validation err a -> These err a
+validationToThese (Failure err) = This err
+validationToThese (Success a) = That a
+
+-- | Nested '>>=' of a These inside some other @m@.
+--
+-- Use if you want to collect errors and successes, and want to chain multiple function returning 'These'.
+thenThese ::
+  (Monad m, Semigroup err) =>
+  (a -> m (These err b)) ->
+  m (These err a) ->
+  m (These err b)
+thenThese f x = do
+  th <- x
+  join <$> traverse f th
+
+-- | Nested validating bind-like combinator inside some other @m@.
+--
+-- Use if you want to collect errors, and want to chain multiple functions returning 'Validation'.
+thenValidate ::
+  (Monad m) =>
+  (a -> m (Validation err b)) ->
+  m (Validation err a) ->
+  m (Validation err b)
+thenValidate f x =
+  eitherToValidation <$> do
+    x' <- validationToEither <$> x
+    case x' of
+      Left err -> pure $ Left err
+      Right a -> validationToEither <$> f a
+
+-- | Put the text to @stderr@.
+putStderrLn :: Text -> IO ()
+putStderrLn msg =
+  System.IO.hPutStrLn System.IO.stderr $ textToString msg
+
+exitWithMessage :: Text -> IO a
+exitWithMessage msg = do
+  putStderrLn msg
+  System.Exit.exitWith $ System.Exit.ExitFailure (-1)
+
+-- | Run some function producing applicative over a traversable data structure,
+-- then collect the results in a Monoid.
+--
+-- Very helpful with side-effecting functions returning @(Validation err a)@:
+--
+-- @
+-- let
+--   f :: Text -> IO (Validation (NonEmpty Error) Text)
+--   f t = pure $ if t == "foo" then Success t else Failure (singleton ("not foo: " <> t))
+--
+-- in traverseFold f [ "foo", "bar", "baz" ]
+--   == Failure ("not foo bar" :| ["not foo baz"])
+-- @
+--
+-- … since @(Semigroup err => Validation err a)@ is a @Semigroup@/@Monoid@ itself.
+traverseFold :: (Applicative ap, Traversable t, Monoid m) => (a -> ap m) -> t a -> ap m
+traverseFold f xs =
+  -- note: could be weakened to (Foldable t) via `getAp . foldMap (Ap . f)`
+  fold <$> traverse f xs
+{-# INLINE traverseFold #-}
+
+-- | Like 'traverseFold', but fold over a semigroup instead of a Monoid, by providing a starting element.
+traverseFoldDefault :: (Applicative ap, Traversable t, Semigroup m) => m -> (a -> ap m) -> t a -> ap m
+traverseFoldDefault def f xs = foldDef def <$> traverse f xs
+  where
+    foldDef = foldr (<>)
+{-# INLINE traverseFoldDefault #-}
+
+-- | Same as 'traverseFold', but with a 'Semigroup' and 'Traversable1' restriction.
+traverseFold1 :: (Applicative ap, Traversable1 t, Semigroup s) => (a -> ap s) -> t a -> ap s
+-- note: cannot be weakened to (Foldable1 t) because there is no `Ap` for Semigroup (No `Apply` typeclass)
+traverseFold1 f xs = fold1 <$> traverse f xs
+{-# INLINE traverseFold1 #-}
+
+-- | Use this in places where the code is still to be implemented.
+--
+-- It always type-checks and will show a warning at compile time if it was forgotten in the code.
+--
+-- Use instead of 'error' and 'undefined' for code that hasn’t been written.
+--
+-- Uses the same trick as https://hackage.haskell.org/package/protolude-0.3.0/docs/src/Protolude.Error.html#error
+{-# WARNING todo "'todo' (undefined code) remains in code" #-}
+todo :: forall (r :: RuntimeRep). forall (a :: TYPE r). HasCallStack => a
+todo = raise# (errorCallWithCallStackException "This code was not yet implemented: TODO" ?callStack)
+
+-- | Convert an integer to a 'Natural' if possible
+--
+-- Named the same as the function from "GHC.Natural", but does not crash.
+intToNatural :: Integral a => a -> Maybe Natural
+intToNatural i =
+  if i < 0
+    then Nothing
+    else Just $ fromIntegral i
+
+-- | @inverseFunction f@ creates a function that is the inverse of a given function
+-- @f@. It does so by constructing 'M.Map' internally for each value @f a@. The
+-- implementation makes sure that the 'M.Map' is constructed only once and then
+-- shared for every call.
+--
+-- __Memory usage note:__ don't inverse functions that have types like 'Int'
+-- as their result. In this case the created 'M.Map' will have huge size.
+--
+-- The complexity of reversed mapping is \(\mathcal{O}(\log n)\).
+--
+-- __Performance note:__ make sure to specialize monomorphic type of your functions
+-- that use 'inverseFunction' to avoid 'M.Map' reconstruction.
+--
+-- One of the common 'inverseFunction' use-case is inverting the 'show' or a 'show'-like
+-- function.
+--
+-- >>> data Color = Red | Green | Blue deriving (Show, Enum, Bounded)
+-- >>> parse = inverseFunction show :: String -> Maybe Color
+-- >>> parse "Red"
+-- Just Red
+-- >>> parse "Black"
+-- Nothing
+--
+-- __Correctness note:__ 'inverseFunction' expects /injective function/ as its argument,
+-- i.e. the function must map distinct arguments to distinct values.
+--
+-- Typical usage of this function looks like this:
+--
+-- @
+-- __data__ GhcVer
+--    = Ghc802
+--    | Ghc822
+--    | Ghc844
+--    | Ghc865
+--    | Ghc881
+--    __deriving__ ('Eq', 'Ord', 'Show', 'Enum', 'Bounded')
+--
+-- showGhcVer :: GhcVer -> 'Text'
+-- showGhcVer = \\__case__
+--    Ghc802 -> "8.0.2"
+--    Ghc822 -> "8.2.2"
+--    Ghc844 -> "8.4.4"
+--    Ghc865 -> "8.6.5"
+--    Ghc881 -> "8.8.1"
+--
+-- parseGhcVer :: 'Text' -> 'Maybe' GhcVer
+-- parseGhcVer = 'inverseFunction' showGhcVer
+--
+-- Taken from relude’s @Relude.Extra.Enum@.
+inverseFunction ::
+  forall a k.
+  (Bounded a, Enum a, Ord k) =>
+  (a -> k) ->
+  (k -> Maybe a)
+inverseFunction f k = Map.lookup k $ inverseMap f
+
+-- | Like `inverseFunction`, but instead of returning the function
+-- it returns a mapping from all possible outputs to their possible inputs.
+--
+-- This has the same restrictions of 'inverseFunction'.
+inverseMap ::
+  forall a k.
+  (Bounded a, Enum a, Ord k) =>
+  (a -> k) ->
+  Map k a
+inverseMap f =
+  universe
+    <&> (\a -> (f a, a))
+    & Map.fromList
+  where
+    universe :: [a]
+    universe = [minBound .. maxBound]
+
+-- | If the predicate is true, return the @m@, else 'mempty'.
+--
+-- This can be used (together with `ifExists`) to e.g. create lists with optional elements:
+--
+-- >>> import Data.Monoid (Sum(..))
+--
+-- >>> :{ mconcat [
+--   ifTrue (1 == 1) [1],
+--   [2, 3, 4],
+--   ifTrue False [5],
+-- ]
+-- :}
+-- [1,2,3,4]
+--
+-- Or any other Monoid:
+--
+-- >>> mconcat [ Sum 1, ifTrue (1 == 1) (Sum 2), Sum 3 ]
+
+-- Sum {getSum = 6}
+
+ifTrue :: Monoid m => Bool -> m -> m
+ifTrue pred' m = if pred' then m else mempty
+
+-- | If the given @Maybe@ is @Just@, return the @m@, else return mempty.
+
+-- This can be used (together with `ifTrue`) to e.g. create lists with optional elements:
+--
+-- >>> import Data.Monoid (Sum(..))
+--
+-- >>> :{ mconcat [
+--   ifExists (Just [1]),
+--   [2, 3, 4],
+--   ifExists Nothing,
+-- ]
+-- :}
+-- [1,2,3,4]
+--
+-- Or any other Monoid:
+--
+-- >>> mconcat [ Sum 1, ifExists (Just (Sum 2)), Sum 3 ]
+
+-- Sum {getSum = 6}
+
+ifExists :: Monoid m => Maybe m -> m
+ifExists = fold
diff --git a/users/Profpatsch/my-prelude/Pretty.hs b/users/Profpatsch/my-prelude/Pretty.hs
new file mode 100644
index 000000000000..8046c83e459c
--- /dev/null
+++ b/users/Profpatsch/my-prelude/Pretty.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE ImportQualifiedPost #-}
+{-# LANGUAGE LambdaCase #-}
+
+module Pretty
+  ( -- * Pretty printing for error messages
+    Err,
+    printPretty,
+    showPretty,
+    -- constructors hidden
+    prettyErrs,
+    message,
+    messageString,
+    pretty,
+    prettyString,
+    hscolour',
+  )
+where
+
+import Data.List qualified as List
+import Data.Text qualified as Text
+import Language.Haskell.HsColour
+  ( Output (TTYg),
+    hscolour,
+  )
+import Language.Haskell.HsColour.ANSI (TerminalType (..))
+import Language.Haskell.HsColour.Colourise
+  ( defaultColourPrefs,
+  )
+import MyPrelude
+import System.Console.ANSI (setSGRCode)
+import System.Console.ANSI.Types
+  ( Color (Red),
+    ColorIntensity (Dull),
+    ConsoleLayer (Foreground),
+    SGR (Reset, SetColor),
+  )
+import Text.Nicify (nicify)
+
+-- | Print any 'Show'able type to stderr, formatted nicely and in color. Very helpful for debugging.
+printPretty :: Show a => a -> IO ()
+printPretty a =
+  a & pretty & (: []) & prettyErrs & stringToText & putStderrLn
+
+showPretty :: Show a => a -> Text
+showPretty a = a & pretty & (: []) & prettyErrs & stringToText
+
+-- | Display a list of 'Err's as a colored error message
+-- and abort the test.
+prettyErrs :: [Err] -> String
+prettyErrs errs = res
+  where
+    res = List.intercalate "\n" $ map one errs
+    one = \case
+      ErrMsg s -> color Red s
+      ErrPrettyString s -> prettyShowString s
+    -- Pretty print a String that was produced by 'show'
+    prettyShowString :: String -> String
+    prettyShowString = hscolour' . nicify
+
+-- | Small DSL for pretty-printing errors
+data Err
+  = -- | Message to display in the error
+    ErrMsg String
+  | -- | Pretty print a String that was produced by 'show'
+    ErrPrettyString String
+
+-- | Plain message to display, as 'Text'
+message :: Text -> Err
+message = ErrMsg . Text.unpack
+
+-- | Plain message to display, as 'String'
+messageString :: String -> Err
+messageString = ErrMsg
+
+-- | Any 'Show'able to pretty print
+pretty :: Show a => a -> Err
+pretty x = ErrPrettyString $ show x
+
+-- | Pretty print a String that was produced by 'show'
+prettyString :: String -> Err
+prettyString s = ErrPrettyString s
+
+-- Prettifying Helpers, mostly stolen from
+-- https://hackage.haskell.org/package/hspec-expectations-pretty-diff-0.7.2.5/docs/src/Test.Hspec.Expectations.Pretty.html#prettyColor
+
+hscolour' :: String -> String
+hscolour' =
+  hscolour (TTYg Ansi16Colour) defaultColourPrefs False False "" False
+
+color :: Color -> String -> String
+color c s = setSGRCode [SetColor Foreground Dull c] ++ s ++ setSGRCode [Reset]
diff --git a/users/Profpatsch/my-prelude/README.md b/users/Profpatsch/my-prelude/README.md
new file mode 100644
index 000000000000..2cc068579a52
--- /dev/null
+++ b/users/Profpatsch/my-prelude/README.md
@@ -0,0 +1,42 @@
+# My Haskell Prelude
+
+Contains various modules I’ve found useful when writing Haskell.
+
+## Contents
+
+A short overview:
+
+### `MyPrelude.hs`
+
+A collection of re-exports and extra functions. This does *not* replace the `Prelude` module from `base`, but rather should be imported *in addition* to `Prelude`.
+
+Stuff like bad functions from prelude (partial stuff, or plain horrible stuff) are handled by a custom `.hlint` file, which you can find in [../.hlint.yaml]().
+
+The common style of haskell they try to enable is what I call “left-to-right Haskell”,
+where one mostly prefers forward-chaining operators like `&`/`<&>`/`>>=` to backwards operators like `$`/`<$>`/`<=<`. In addition, all transformation function should follow the scheme of `aToB` instead of `B.fromA`, e.g. `Text.unpack`/`Text.pack` -> `textToString`/`stringToText`. Includes a bunch of text conversion functions one needs all the time, in the same style.
+
+These have been battle-tested in a production codebase of ~30k lines of Haskell.
+
+### `Label.hs`
+
+A very useful collection of anonymous labbeled tuples and enums of size 2 and 3. Assumes GHC >9.2 for `RecordDotSyntax` support.
+
+### `Pretty.hs`
+
+Colorful multiline pretty-printing of Haskell values.
+
+### `Test.hs`
+
+A wrapper around `hspec` which produces colorful test diffs.
+
+### `Aeson.hs`
+
+Helpers around Json parsing.
+
+### `Data.Error.Tree`
+
+Collect errors (from [`Data.Error`](https://hackage.haskell.org/package/error-1.0.0.0/docs/Data-Error.html)) into a tree, then display them in a nested fashion. Super useful for e.g. collecting and displaying nested parsing errors.
+
+### `RunCommand.hs`
+
+A module wrapping the process API with some helpful defaults for executing commands and printing what is executed to stderr.
diff --git a/users/Profpatsch/my-prelude/RunCommand.hs b/users/Profpatsch/my-prelude/RunCommand.hs
new file mode 100644
index 000000000000..5c80eb3aacf4
--- /dev/null
+++ b/users/Profpatsch/my-prelude/RunCommand.hs
@@ -0,0 +1,162 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+
+module RunCommand where
+
+import Data.ByteString qualified as ByteString
+import Data.ByteString.Lazy qualified as Bytes.Lazy
+import Data.Char qualified as Char
+import Data.List qualified as List
+import Data.Text qualified as Text
+import MyPrelude
+import System.Exit qualified as Exit
+import System.IO (Handle)
+import System.Process.Typed qualified as Process
+import Prelude hiding (log)
+
+-- | Given a a command, the executable and arguments,
+-- spawn the tool as subprocess and collect its stdout (stderr will go to our stderr).
+
+-- Will strip the stdout of trailing newlines.
+--
+-- If the executable is not a path, it will be resolved via the @PATH@ environment variable.
+runCommand :: MonadIO m => FilePath -> [Text] -> m (Exit.ExitCode, ByteString)
+runCommand executable args = do
+  let bashArgs = prettyArgsForBash ((executable & stringToText) : args)
+  log [fmt|Running: $ {bashArgs}|]
+  Process.proc
+    executable
+    (args <&> textToString)
+    & Process.readProcessStdout
+    <&> second toStrictBytes
+    <&> second stripWhitespaceFromEnd
+
+-- | Given a a command, the executable and arguments,
+-- spawn the tool as subprocess and run it to conclusion.
+--
+-- If the executable is not a path, it will be resolved via the @PATH@ environment variable.
+runCommandNoStdout :: MonadIO m => FilePath -> [Text] -> m Exit.ExitCode
+runCommandNoStdout executable args = do
+  let bashArgs = prettyArgsForBash ((executable & stringToText) : args)
+  log [fmt|Running: $ {bashArgs}|]
+  Process.proc
+    executable
+    (args <&> textToString)
+    & Process.runProcess
+
+-- TODO: This is reversing the whole string *twice*. Can we strip from end without doing that?
+stripWhitespaceFromEnd :: ByteString -> ByteString
+stripWhitespaceFromEnd = ByteString.reverse . ByteString.dropWhile (\w -> w == charToWordUnsafe '\n') . ByteString.reverse
+
+-- | Like `runCommand`, but takes a Bytestring that provides the command with streamed input on stdin.
+runCommandWithStdin :: MonadIO m => FilePath -> [Text] -> Bytes.Lazy.ByteString -> m (Exit.ExitCode, ByteString)
+runCommandWithStdin executable args stdin = do
+  let bashArgs = prettyArgsForBash ((executable & stringToText) : args)
+  log [fmt|Running: $ {bashArgs}|]
+  Process.proc
+    executable
+    (args <&> textToString)
+    & Process.setStdin (Process.byteStringInput stdin)
+    & Process.readProcessStdout
+    <&> second toStrictBytes
+    <&> second stripWhitespaceFromEnd
+
+-- | Like `runCommand`, but takes a Bytestring that provides the command with streamed input on stdin.
+runCommandWithStdinNoStdout :: MonadIO m => FilePath -> [Text] -> Bytes.Lazy.ByteString -> m Exit.ExitCode
+runCommandWithStdinNoStdout executable args stdin = do
+  let bashArgs = prettyArgsForBash ((executable & stringToText) : args)
+  log [fmt|Running: $ {bashArgs}|]
+  Process.proc
+    executable
+    (args <&> textToString)
+    & Process.setStdin (Process.byteStringInput stdin)
+    & Process.runProcess
+
+-- | Like 'runCommandWithStdin' but exit if the command returns a non-0 status.
+runCommandWithStdinExpect0 :: MonadIO m => FilePath -> [Text] -> Bytes.Lazy.ByteString -> m ByteString
+runCommandWithStdinExpect0 executable args stdin =
+  runCommandWithStdin executable args stdin >>= \case
+    (ex, stdout) -> do
+      checkStatus0 executable ex
+      pure stdout
+
+-- | Like 'runCommandWithStdinNoStdout' but exit if the command returns a non-0 status.
+runCommandWithStdinNoStdoutExpect0 :: MonadIO m => FilePath -> [Text] -> Bytes.Lazy.ByteString -> m ()
+runCommandWithStdinNoStdoutExpect0 executable args stdin =
+  runCommandWithStdinNoStdout executable args stdin
+    >>= checkStatus0 executable
+
+-- | Like 'runCommandExpect0', but don’t capture stdout,
+-- connect stdin and stdout to the command until it returns.
+--
+-- This is for interactive subcommands.
+runCommandInteractiveExpect0 :: MonadIO m => FilePath -> [Text] -> m ()
+runCommandInteractiveExpect0 executable args = do
+  let bashArgs = prettyArgsForBash ((executable & stringToText) : args)
+  log [fmt|Running interactively: $ {bashArgs}|]
+  ( liftIO $
+      Process.runProcess $
+        Process.proc
+          executable
+          (args <&> textToString)
+    )
+    >>= checkStatus0 executable
+
+-- | Given a name of a command, the executable and arguments,
+-- spawn the tool as subprocess and pipe its stdout to the given 'Handle'.
+--
+-- If the executable is not a path, it will be resolved via the @PATH@ environment variable.
+runCommandPipeToHandle :: MonadIO m => FilePath -> [Text] -> Handle -> m Exit.ExitCode
+runCommandPipeToHandle executable args handle = do
+  -- TODO log the output file?
+  let bashArgs = prettyArgsForBash ((executable & stringToText) : args)
+  log [fmt|Running: $ {bashArgs}|]
+  liftIO $
+    Process.runProcess
+      ( Process.proc
+          executable
+          (args <&> textToString)
+          & Process.setStdout (Process.useHandleClose handle)
+      )
+
+-- | Check whether a command exited 0 or crash.
+checkStatus0 :: MonadIO m => FilePath -> Exit.ExitCode -> m ()
+checkStatus0 executable = \case
+  Exit.ExitSuccess -> pure ()
+  Exit.ExitFailure status -> do
+    logCritical [fmt|Command `{executable}` did not exit with status 0 (success), but status {status}|]
+
+log :: MonadIO m => Text -> m ()
+log = liftIO . putStderrLn
+
+-- | Log the message on the normal logging level & exit the program
+logCritical :: MonadIO m => Text -> m b
+logCritical msg = do
+  liftIO $ putStderrLn msg
+  liftIO $ Exit.exitWith (Exit.ExitFailure 1)
+
+-- | Pretty print a command line in a way that can be copied to bash.
+prettyArgsForBash :: [Text] -> Text
+prettyArgsForBash = Text.intercalate " " . map simpleBashEscape
+
+-- | Simple escaping for bash words. If they contain anything that’s not ascii chars
+-- and a bunch of often-used special characters, put the word in single quotes.
+simpleBashEscape :: Text -> Text
+simpleBashEscape t = do
+  case Text.find (not . isSimple) t of
+    Just _ -> escapeSingleQuote t
+    Nothing -> t
+  where
+    -- any word that is just ascii characters is simple (no spaces or control characters)
+    -- or contains a few often-used characters like - or .
+    isSimple c =
+      Char.isAsciiLower c
+        || Char.isAsciiUpper c
+        || Char.isDigit c
+        -- These are benign, bash will not interpret them as special characters.
+        || List.elem c ['-', '.', ':', '/']
+    -- Put the word in single quotes
+    -- If there is a single quote in the word,
+    -- close the single quoted word, add a single quote, open the word again
+    escapeSingleQuote t' = "'" <> Text.replace "'" "'\\''" t' <> "'"
diff --git a/users/Profpatsch/my-prelude/Test.hs b/users/Profpatsch/my-prelude/Test.hs
new file mode 100644
index 000000000000..862ee16c255d
--- /dev/null
+++ b/users/Profpatsch/my-prelude/Test.hs
@@ -0,0 +1,115 @@
+{-# LANGUAGE LambdaCase #-}
+
+{- Generate Test suites.
+
+Restricted version of hspec, introduction: http://hspec.github.io/getting-started.html
+-}
+module Test
+  ( Spec,
+    runTest,
+    testMain,
+
+    -- * Structure
+    describe,
+    it,
+
+    -- * Expectations
+    Expectation,
+    testOk,
+    testErr,
+    shouldBe,
+    shouldNotBe,
+    shouldSatisfy,
+    shouldNotSatisfy,
+
+    -- * Setup & Teardown (hooks http://hspec.github.io/writing-specs.html#using-hooks)
+    before,
+    before_,
+    beforeWith,
+    beforeAll,
+    beforeAll_,
+    beforeAllWith,
+    after,
+    after_,
+    afterAll,
+    afterAll_,
+    around,
+    around_,
+    aroundWith,
+    aroundAll,
+    aroundAllWith,
+
+    -- * Common helpful predicates (use with 'shouldSatisfy')
+    isRight,
+    isLeft,
+
+    -- * Pretty printing of errors
+    errColored,
+    module Pretty,
+  )
+where
+
+-- export more expectations if needed
+
+import Data.Either
+  ( isLeft,
+    isRight,
+  )
+import Pretty
+import Test.Hspec
+  ( Expectation,
+    HasCallStack,
+    Spec,
+    after,
+    afterAll,
+    afterAll_,
+    after_,
+    around,
+    aroundAll,
+    aroundAllWith,
+    aroundWith,
+    around_,
+    before,
+    beforeAll,
+    beforeAllWith,
+    beforeAll_,
+    beforeWith,
+    before_,
+    describe,
+    hspec,
+    it,
+  )
+import Test.Hspec.Expectations.Pretty
+  ( expectationFailure,
+    shouldBe,
+    shouldNotBe,
+    shouldNotSatisfy,
+    shouldSatisfy,
+  )
+
+-- | Run a test directly (e.g. from the repl)
+runTest :: Spec -> IO ()
+runTest = hspec
+
+-- | Run a testsuite
+testMain ::
+  -- | Name of the test suite
+  String ->
+  -- | The tests in this test module
+  Spec ->
+  IO ()
+testMain testSuiteName tests = hspec $ describe testSuiteName tests
+
+-- | test successful
+testOk :: Expectation
+testOk = pure ()
+
+-- | Abort the test with an error message.
+-- If you want to display a Haskell type, use `errColored`.
+testErr :: HasCallStack => String -> Expectation
+testErr = expectationFailure
+
+-- | Display a list of 'Err's as a colored error message
+-- and abort the test.
+errColored :: [Pretty.Err] -> Expectation
+errColored = testErr . Pretty.prettyErrs
diff --git a/users/Profpatsch/my-prelude/default.nix b/users/Profpatsch/my-prelude/default.nix
new file mode 100644
index 000000000000..4d854b194b39
--- /dev/null
+++ b/users/Profpatsch/my-prelude/default.nix
@@ -0,0 +1,41 @@
+{ depot, pkgs, lib, ... }:
+
+pkgs.haskellPackages.mkDerivation {
+  pname = "my-prelude";
+  version = "0.0.1-unreleased";
+
+  src = depot.users.Profpatsch.exactSource ./. [
+    ./my-prelude.cabal
+    ./MyPrelude.hs
+    ./Label.hs
+    ./Pretty.hs
+    ./Data/Error/Tree.hs
+    ./Aeson.hs
+    ./RunCommand.hs
+    ./Test.hs
+  ];
+
+  isLibrary = true;
+
+  libraryHaskellDepends = [
+    pkgs.haskellPackages.aeson
+    pkgs.haskellPackages.aeson-better-errors
+    pkgs.haskellPackages.PyF
+    pkgs.haskellPackages.errors
+    pkgs.haskellPackages.profunctors
+    pkgs.haskellPackages.semigroupoids
+    pkgs.haskellPackages.these
+    pkgs.haskellPackages.validation-selective
+    pkgs.haskellPackages.error
+    pkgs.haskellPackages.hspec
+    pkgs.haskellPackages.hspec-expectations-pretty-diff
+    pkgs.haskellPackages.hscolour
+    pkgs.haskellPackages.nicify-lib
+    pkgs.haskellPackages.typed-process
+    pkgs.haskellPackages.ansi-terminal
+    pkgs.haskellPackages.vector
+  ];
+
+  license = lib.licenses.mit;
+
+}
diff --git a/users/Profpatsch/my-prelude/my-prelude.cabal b/users/Profpatsch/my-prelude/my-prelude.cabal
new file mode 100644
index 000000000000..94f9c9e77417
--- /dev/null
+++ b/users/Profpatsch/my-prelude/my-prelude.cabal
@@ -0,0 +1,44 @@
+cabal-version:      2.4
+name:               my-prelude
+version:            0.0.1.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+library
+    exposed-modules:
+      MyPrelude
+      Label
+      Pretty
+      Data.Error.Tree
+      Aeson
+      RunCommand
+      Test
+
+    -- Modules included in this executable, other than Main.
+    -- other-modules:
+
+    -- LANGUAGE extensions used by modules in this package.
+    -- other-extensions:
+    build-depends:
+       base >=4.15 && <5
+     , aeson
+     , aeson-better-errors
+     , PyF
+     , validation-selective
+     , these
+     , text
+     , semigroupoids
+     , profunctors
+     , containers
+     , error
+     , exceptions
+     , bytestring
+     , mtl
+     , hspec
+     , hspec-expectations-pretty-diff
+     , hscolour
+     , nicify-lib
+     , typed-process
+     , ansi-terminal
+     , vector
+    default-language: GHC2021
diff --git a/users/Profpatsch/netencode/Netencode.hs b/users/Profpatsch/netencode/Netencode.hs
new file mode 100644
index 000000000000..36d3907ffc0a
--- /dev/null
+++ b/users/Profpatsch/netencode/Netencode.hs
@@ -0,0 +1,445 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+
+module Netencode where
+
+import Control.Applicative (many)
+import Data.Attoparsec.ByteString qualified as Atto
+import Data.Attoparsec.ByteString.Char8 qualified as Atto.Char
+import Data.ByteString qualified as ByteString
+import Data.ByteString.Builder (Builder)
+import Data.ByteString.Builder qualified as Builder
+import Data.ByteString.Lazy qualified as ByteString.Lazy
+import Data.Coerce (coerce)
+import Data.Fix (Fix (Fix))
+import Data.Fix qualified as Fix
+import Data.Functor.Classes (Eq1 (liftEq))
+import Data.Int (Int16, Int32, Int64, Int8)
+import Data.List.NonEmpty (nonEmpty)
+import Data.Map.NonEmpty (NEMap)
+import Data.Map.NonEmpty qualified as NEMap
+import Data.Maybe (fromMaybe)
+import Data.Semigroup qualified as Semi
+import Data.String (IsString)
+import Data.Word (Word16, Word32, Word64)
+import GHC.Exts (fromString)
+import Hedgehog qualified as Hedge
+import Hedgehog.Gen qualified as Gen
+import Hedgehog.Range qualified as Range
+import MyPrelude
+import Text.Show.Deriving
+import Prelude hiding (sum)
+
+-- | Netencode type base functor.
+--
+-- Recursive elements have a @rec@.
+data TF rec
+  = -- | Unit value
+    Unit
+  | -- | Boolean (2^1)
+    N1 Bool
+  | -- | Byte (2^3)
+    N3 Word8
+  | -- | 64-bit Natural (2^6)
+    N6 Word64
+  | -- | 64-bit Integer (2^6)
+    I6 Int64
+  | -- | Unicode Text
+    Text Text
+  | -- | Arbitrary Bytestring
+    Bytes ByteString
+  | -- | A constructor of a(n open) Sum
+    Sum (Tag Text rec)
+  | -- | Record
+    Record (NEMap Text rec)
+  | -- | List
+    List [rec]
+  deriving stock (Show, Eq, Functor)
+
+instance Eq1 TF where
+  liftEq _ Unit Unit = True
+  liftEq _ (N1 b) (N1 b') = b == b'
+  liftEq _ (N3 w8) (N3 w8') = w8 == w8'
+  liftEq _ (N6 w64) (N6 w64') = w64 == w64'
+  liftEq _ (I6 i64) (I6 i64') = i64 == i64'
+  liftEq _ (Text t) (Text t') = t == t'
+  liftEq _ (Bytes b) (Bytes b') = b == b'
+  liftEq eq (Sum t) (Sum t') = eq (t & tagVal) (t' & tagVal)
+  liftEq eq (Record m) (Record m') = liftEq eq m m'
+  liftEq eq (List xs) (List xs') = liftEq eq xs xs'
+  liftEq _ _ _ = False
+
+-- | A tagged value
+data Tag tag val = Tag
+  { tagTag :: tag,
+    tagVal :: val
+  }
+  deriving stock (Show, Eq, Functor)
+
+$(Text.Show.Deriving.deriveShow1 ''Tag)
+$(Text.Show.Deriving.deriveShow1 ''TF)
+
+-- | The Netencode type
+newtype T = T {unT :: Fix TF}
+  deriving stock (Eq, Show)
+
+-- | Create a unit
+unit :: T
+unit = T $ Fix Unit
+
+-- | Create a boolean
+n1 :: Bool -> T
+n1 = T . Fix . N1
+
+-- | Create a byte
+n3 :: Word8 -> T
+n3 = T . Fix . N3
+
+-- | Create a 64-bit natural
+n6 :: Word64 -> T
+n6 = T . Fix . N6
+
+-- | Create a 64-bit integer
+i6 :: Int64 -> T
+i6 = T . Fix . I6
+
+-- | Create a UTF-8 unicode text
+text :: Text -> T
+text = T . Fix . Text
+
+-- | Create an arbitrary bytestring
+bytes :: ByteString -> T
+bytes = T . Fix . Bytes
+
+-- | Create a tagged value from a tag name and a value
+tag :: Text -> T -> T
+tag key val = T $ Fix $ Sum $ coerce @(Tag Text T) @(Tag Text (Fix TF)) $ Tag key val
+
+-- | Create a record from a non-empty map
+record :: NEMap Text T -> T
+record = T . Fix . Record . coerce @(NEMap Text T) @(NEMap Text (Fix TF))
+
+-- | Create a list
+list :: [T] -> T
+list = T . Fix . List . coerce @[T] @([Fix TF])
+
+-- | Stable encoding of a netencode value. Record keys will be sorted lexicographically ascending.
+netencodeEncodeStable :: T -> Builder
+netencodeEncodeStable (T fix) = Fix.foldFix (netencodeEncodeStableF id) fix
+
+-- | Stable encoding of a netencode functor value. Record keys will be sorted lexicographically ascending.
+--
+-- The given function is used for encoding the recursive values.
+netencodeEncodeStableF :: (rec -> Builder) -> TF rec -> Builder
+netencodeEncodeStableF inner tf = builder go
+  where
+    -- TODO: directly pass in BL?
+    innerBL = fromBuilder . inner
+    go = case tf of
+      Unit -> "u,"
+      N1 False -> "n1:0,"
+      N1 True -> "n1:1,"
+      N3 w8 -> "n3:" <> fromBuilder (Builder.word8Dec w8) <> ","
+      N6 w64 -> "n6:" <> fromBuilder (Builder.word64Dec w64) <> ","
+      I6 i64 -> "i6:" <> fromBuilder (Builder.int64Dec i64) <> ","
+      Text t ->
+        let b = fromText t
+         in "t" <> builderLenDec b <> ":" <> b <> ","
+      Bytes b -> "b" <> builderLenDec (fromByteString b) <> ":" <> fromByteString b <> ","
+      Sum (Tag key val) -> encTag key val
+      Record m ->
+        -- NEMap uses Map internally, and that folds in lexicographic ascending order over the key.
+        -- Since these are `Text` in our case, this is stable.
+        let mBuilder = m & NEMap.foldMapWithKey encTag
+         in "{" <> builderLenDec mBuilder <> ":" <> mBuilder <> "}"
+      List xs ->
+        let xsBuilder = xs <&> innerBL & mconcat
+         in "[" <> builderLenDec xsBuilder <> ":" <> xsBuilder <> "]"
+      where
+        encTag key val =
+          let bKey = fromText key
+           in "<" <> builderLenDec bKey <> ":" <> bKey <> "|" <> innerBL val
+
+-- | A builder that knows its own size in bytes
+newtype BL = BL (Builder, Semi.Sum Natural)
+  deriving newtype (Monoid, Semigroup)
+
+instance IsString BL where
+  fromString s =
+    BL
+      ( fromString @Builder s,
+        fromString @ByteString s
+          & ByteString.length
+          & intToNatural
+          & fromMaybe 0
+          & Semi.Sum
+      )
+
+-- | Retrieve the builder
+builder :: BL -> Builder
+builder (BL (b, _)) = b
+
+-- | Retrieve the bytestring length
+builderLen :: BL -> Natural
+builderLen (BL (_, len)) = Semi.getSum $ len
+
+-- | Take a 'BL' and create a new 'BL' that represents the length as a decimal integer
+builderLenDec :: BL -> BL
+builderLenDec (BL (_, len)) =
+  let b = Builder.intDec $ (len & Semi.getSum & fromIntegral @Natural @Int)
+   in b & fromBuilder
+
+-- | Create a 'BL' from a 'Builder'.
+--
+-- Not efficient, goes back to a lazy bytestring to get the length
+fromBuilder :: Builder -> BL
+fromBuilder b =
+  BL
+    ( b,
+      b
+        & Builder.toLazyByteString
+        & ByteString.Lazy.length
+        & fromIntegral @Int64 @Natural
+        & Semi.Sum
+    )
+
+-- | Create a 'BL' from a 'ByteString'.
+fromByteString :: ByteString -> BL
+fromByteString b =
+  BL
+    ( Builder.byteString b,
+      b
+        & ByteString.length
+        & fromIntegral @Int @Natural
+        & Semi.Sum
+    )
+
+-- | Create a 'BL' from a 'Text'.
+fromText :: Text -> BL
+fromText t = t & textToBytesUtf8 & fromByteString
+
+-- | Parser for a netencode value.
+netencodeParser :: Atto.Parser T
+netencodeParser = T <$> go
+  where
+    go = Fix <$> netencodeParserF go
+
+-- | Parser for one level of a netencode value. Requires a parser for the recursion.
+netencodeParserF :: Atto.Parser rec -> Atto.Parser (TF rec)
+netencodeParserF inner = do
+  typeTag <- Atto.Char.anyChar
+  case typeTag of
+    't' -> Text <$> textParser
+    'b' -> Bytes <$> bytesParser
+    'u' -> unitParser
+    '<' -> Sum <$> tagParser
+    '{' -> Record <$> recordParser
+    '[' -> List <$> listParser
+    'n' -> naturalParser
+    'i' -> I6 <$> intParser
+    c -> fail ([c] <> " is not a valid netencode tag")
+  where
+    bytesParser = do
+      len <- boundedDecimalFail Atto.<?> "bytes is missing a digit specifying the length"
+      _ <- Atto.Char.char ':' Atto.<?> "bytes did not have : after length"
+      bytes' <- Atto.take len
+      _ <- Atto.Char.char ',' Atto.<?> "bytes did not end with ,"
+      pure bytes'
+
+    textParser = do
+      len <- boundedDecimalFail Atto.<?> "text is missing a digit specifying the length"
+      _ <- Atto.Char.char ':' Atto.<?> "text did not have : after length"
+      text' <-
+        Atto.take len <&> bytesToTextUtf8 >>= \case
+          Left err -> fail [fmt|cannot decode text as utf8: {err & prettyError}|]
+          Right t -> pure t
+      _ <- Atto.Char.char ',' Atto.<?> "text did not end with ,"
+      pure text'
+
+    unitParser = do
+      _ <- Atto.Char.char ',' Atto.<?> "unit did not end with ,"
+      pure $ Unit
+
+    tagParser = do
+      len <- boundedDecimalFail Atto.<?> "tag is missing a digit specifying the length"
+      _ <- Atto.Char.char ':' Atto.<?> "tag did not have : after length"
+      tagTag <-
+        Atto.take len <&> bytesToTextUtf8 >>= \case
+          Left err -> fail [fmt|cannot decode tag key as utf8: {err & prettyError}|]
+          Right t -> pure t
+      _ <- Atto.Char.char '|' Atto.<?> "tag was missing the key/value separator (|)"
+      tagVal <- inner
+      pure $ Tag {..}
+
+    recordParser = do
+      -- TODO: the record does not use its inner length because we are descending into the inner parsers.
+      -- This is a smell! In theory it can be used to skip parsing the whole inner keys.
+      _len <- boundedDecimalFail Atto.<?> "record is missing a digit specifying the length"
+      _ <- Atto.Char.char ':' Atto.<?> "record did not have : after length"
+      record' <-
+        many (Atto.Char.char '<' >> tagParser) <&> nonEmpty >>= \case
+          Nothing -> fail "record is not allowed to have 0 elements"
+          Just tags ->
+            pure $
+              tags
+                <&> (\t -> (t & tagTag, t & tagVal))
+                -- later keys are preferred if they are duplicates, according to the standard
+                & NEMap.fromList
+      _ <- Atto.Char.char '}' Atto.<?> "record did not end with }"
+      pure record'
+
+    listParser = do
+      -- TODO: the list does not use its inner length because we are descending into the inner parsers.
+      -- This is a smell! In theory it can be used to skip parsing the whole inner keys.
+      _len <- boundedDecimalFail Atto.<?> "list is missing a digit specifying the length"
+      _ <- Atto.Char.char ':' Atto.<?> "list did not have : after length"
+      -- TODO: allow empty lists?
+      list' <- many inner
+      _ <- Atto.Char.char ']' Atto.<?> "list did not end with ]"
+      pure list'
+
+    intParser = do
+      let p :: forall parseSize. (Bounded parseSize, Integral parseSize) => (Integer -> Atto.Parser Int64)
+          p n = do
+            _ <- Atto.Char.char ':' Atto.<?> [fmt|i{n & show} did not have : after length|]
+            isNegative <- Atto.option False (Atto.Char.char '-' <&> \_c -> True)
+            int <-
+              boundedDecimal @parseSize >>= \case
+                Nothing -> fail [fmt|cannot parse into i{n & show}, the number is too big (would overflow)|]
+                Just i ->
+                  pure $
+                    if isNegative
+                      then -- TODO: this should alread be done in the decimal parser, @minBound@ cannot be parsed cause it’s one more than @(-maxBound)@!
+                        (-i)
+                      else i
+            _ <- Atto.Char.char ',' Atto.<?> [fmt|i{n & show} did not end with ,|]
+            pure $ fromIntegral @parseSize @Int64 int
+      digit <- Atto.Char.digit
+      case digit of
+        -- TODO: separate parser for i1 and i2 that makes sure the boundaries are right!
+        '1' -> p @Int8 1
+        '2' -> p @Int8 2
+        '3' -> p @Int8 3
+        '4' -> p @Int16 4
+        '5' -> p @Int32 5
+        '6' -> p @Int64 6
+        '7' -> fail [fmt|i parser only supports numbers up to size 6, was 7|]
+        '8' -> fail [fmt|i parser only supports numbers up to size 6, was 8|]
+        '9' -> fail [fmt|i parser only supports numbers up to size 6, was 9|]
+        o -> fail [fmt|i number with length {o & show} not possible|]
+
+    naturalParser = do
+      let p :: forall parseSize finalSize. (Bounded parseSize, Integral parseSize, Num finalSize) => (Integer -> Atto.Parser finalSize)
+          p n = do
+            _ <- Atto.Char.char ':' Atto.<?> [fmt|n{n & show} did not have : after length|]
+            int <-
+              boundedDecimal @parseSize >>= \case
+                Nothing -> fail [fmt|cannot parse into n{n & show}, the number is too big (would overflow)|]
+                Just i -> pure i
+
+            _ <- Atto.Char.char ',' Atto.<?> [fmt|n{n & show} did not end with ,|]
+            pure $ fromIntegral @parseSize @finalSize int
+      let b n = do
+            _ <- Atto.Char.char ':' Atto.<?> [fmt|n{n & show} did not have : after length|]
+            bool <-
+              (Atto.Char.char '0' >> pure False)
+                <|> (Atto.Char.char '1' >> pure True)
+            _ <- Atto.Char.char ',' Atto.<?> [fmt|n{n & show} did not end with ,|]
+            pure bool
+
+      digit <- Atto.Char.digit
+      case digit of
+        -- TODO: separate parser for n1 and n2 that makes sure the boundaries are right!
+        '1' -> N1 <$> b 1
+        '2' -> N3 <$> p @Word8 @Word8 2
+        '3' -> N3 <$> p @Word8 @Word8 3
+        '4' -> N6 <$> p @Word16 @Word64 4
+        '5' -> N6 <$> p @Word32 @Word64 5
+        '6' -> N6 <$> p @Word64 @Word64 6
+        '7' -> fail [fmt|n parser only supports numbers up to size 6, was 7|]
+        '8' -> fail [fmt|n parser only supports numbers up to size 6, was 8|]
+        '9' -> fail [fmt|n parser only supports numbers up to size 6, was 9|]
+        o -> fail [fmt|n number with length {o & show} not possible|]
+
+-- | Parser for a bounded decimal that does not overflow the decimal.
+--
+--  via https://www.extrema.is/blog/2021/10/20/parsing-bounded-integers
+boundedDecimal :: forall a. (Bounded a, Integral a) => Atto.Parser (Maybe a)
+boundedDecimal = do
+  i :: Integer <- decimal
+  pure $
+    if (i :: Integer) > fromIntegral (maxBound :: a)
+      then Nothing
+      else Just $ fromIntegral i
+  where
+    -- Copied from @Attoparsec.Text@ and adjusted to bytestring
+    decimal :: (Integral a2) => Atto.Parser a2
+    decimal = ByteString.foldl' step 0 <$> Atto.Char.takeWhile1 Atto.Char.isDigit
+      where
+        step a c = a * 10 + fromIntegral (c - 48)
+{-# SPECIALIZE boundedDecimal :: Atto.Parser (Maybe Int) #-}
+{-# SPECIALIZE boundedDecimal :: Atto.Parser (Maybe Int64) #-}
+{-# SPECIALIZE boundedDecimal :: Atto.Parser (Maybe Word8) #-}
+{-# SPECIALIZE boundedDecimal :: Atto.Parser (Maybe Word64) #-}
+
+-- | 'boundedDecimal', but fail the parser if the decimal overflows.
+boundedDecimalFail :: Atto.Parser Int
+boundedDecimalFail =
+  boundedDecimal >>= \case
+    Nothing -> fail "decimal out of range"
+    Just a -> pure a
+
+-- | Hedgehog generator for a netencode value.
+genNetencode :: Hedge.MonadGen m => m T
+genNetencode =
+  Gen.recursive
+    Gen.choice
+    [ -- these are bundled into one Gen, so that scalar elements get chosen less frequently, and the generator produces nicely nested examples
+      Gen.frequency
+        [ (1, pure unit),
+          (1, n1 <$> Gen.bool),
+          (1, n3 <$> Gen.element [0, 1, 5]),
+          (1, n6 <$> Gen.element [0, 1, 5]),
+          (1, i6 <$> Gen.element [-1, 1, 5]),
+          (2, text <$> Gen.text (Range.linear 1 10) Gen.lower),
+          (2, bytes <$> Gen.bytes (Range.linear 1 10))
+        ]
+    ]
+    [ do
+        key <- Gen.text (Range.linear 3 10) Gen.lower
+        val <- genNetencode
+        pure $ tag key val,
+      record
+        <$> ( let k = Gen.text (Range.linear 3 10) Gen.lower
+                  v = genNetencode
+               in NEMap.insertMap
+                    <$> k
+                    <*> v
+                    <*> ( (Gen.map (Range.linear 0 3)) $
+                            (,) <$> k <*> v
+                        )
+            )
+    ]
+
+-- | Hedgehog property: encoding a netencode value and parsing it again returns the same result.
+prop_netencodeRoundtrip :: Hedge.Property
+prop_netencodeRoundtrip = Hedge.property $ do
+  enc <- Hedge.forAll genNetencode
+  ( Atto.parseOnly
+      netencodeParser
+      ( netencodeEncodeStable enc
+          & Builder.toLazyByteString
+          & toStrictBytes
+      )
+    )
+    Hedge.=== (Right enc)
diff --git a/users/Profpatsch/netencode/Netencode/Parse.hs b/users/Profpatsch/netencode/Netencode/Parse.hs
new file mode 100644
index 000000000000..de313571f713
--- /dev/null
+++ b/users/Profpatsch/netencode/Netencode/Parse.hs
@@ -0,0 +1,112 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE GHC2021 #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedRecordDot #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE NoFieldSelectors #-}
+{-# OPTIONS_GHC -Wall #-}
+
+module Netencode.Parse where
+
+import Control.Category qualified
+import Control.Selective (Selective)
+import Data.Error.Tree
+import Data.Fix (Fix (..))
+import Data.Functor.Compose
+import Data.List qualified as List
+import Data.Map.NonEmpty (NEMap)
+import Data.Map.NonEmpty qualified as NEMap
+import Data.Semigroupoid qualified as Semigroupiod
+import Data.Semigroupoid qualified as Semigroupoid
+import Data.Text qualified as Text
+import MyPrelude
+import Netencode qualified
+import Prelude hiding (log)
+
+newtype Parse from to
+  = -- TODO: the way @Context = [Text]@ has to be forwarded to everything is kinda shitty.
+    -- This is essentially just a difference list, and can probably be treated as a function in the output?
+    Parse (([Text], from) -> Validation (NonEmpty ErrorTree) ([Text], to))
+  deriving
+    (Functor, Applicative, Selective)
+    via ( Compose
+            ( Compose
+                ((->) ([Text], from))
+                (Validation (NonEmpty ErrorTree))
+            )
+            ((,) [Text])
+        )
+
+runParse :: Error -> Parse from to -> from -> Either ErrorTree to
+runParse errMsg parser t =
+  (["$"], t)
+    & runParse' parser
+    <&> snd
+    & first (nestedMultiError errMsg)
+    & validationToEither
+
+runParse' :: Parse from to -> ([Text], from) -> Validation (NonEmpty ErrorTree) ([Text], to)
+runParse' (Parse f) from = f from
+
+instance Semigroupoid Parse where
+  o p2 p1 = Parse $ \from -> case runParse' p1 from of
+    Failure err -> Failure err
+    Success to1 -> runParse' p2 to1
+
+instance Category Parse where
+  (.) = Semigroupoid.o
+  id = Parse $ \t -> Success t
+
+parseEither :: (([Text], from) -> Either ErrorTree ([Text], to)) -> Parse from to
+parseEither f = Parse $ \from -> f from & eitherToListValidation
+
+tAs :: (Netencode.TF (Fix Netencode.TF) -> Either ([Text] -> ErrorTree) to) -> Parse Netencode.T to
+tAs f = parseEither ((\(context, Netencode.T (Fix tf)) -> f tf & bimap ($ context) (context,)))
+
+key :: Text -> Parse (NEMap Text to) to
+key name = parseEither $ \(context, rec) ->
+  rec
+    & NEMap.lookup name
+    & annotate (errorTreeContext (showContext context) [fmt|Key "{name}" does not exist|])
+    <&> (addContext name context,)
+
+showContext :: [Text] -> Text
+showContext context = context & List.reverse & Text.intercalate "."
+
+addContext :: a -> [a] -> [a]
+addContext = (:)
+
+asText :: Parse Netencode.T Text
+asText = tAs $ \case
+  Netencode.Text t -> pure t
+  other -> typeError "of text" other
+
+asBytes :: Parse Netencode.T ByteString
+asBytes = tAs $ \case
+  Netencode.Bytes b -> pure b
+  other -> typeError "of bytes" other
+
+asRecord :: Parse Netencode.T (NEMap Text (Netencode.T))
+asRecord = tAs $ \case
+  Netencode.Record rec -> pure (rec <&> Netencode.T)
+  other -> typeError "a record" other
+
+typeError :: Text -> Netencode.TF ignored -> (Either ([Text] -> ErrorTree) b)
+typeError should is = do
+  let otherS = is <&> (\_ -> ("…" :: String)) & show
+  Left $ \context -> errorTreeContext (showContext context) [fmt|Value is not {should}, but a {otherS}|]
+
+orThrowParseError ::
+  Parse (Either Error to) to
+orThrowParseError = Parse $ \case
+  (context, Left err) ->
+    err
+      & singleError
+      & errorTreeContext (showContext context)
+      & singleton
+      & Failure
+  (context, Right to) -> Success (context, to)
diff --git a/users/Profpatsch/netencode/README.md b/users/Profpatsch/netencode/README.md
new file mode 100644
index 000000000000..840ffaedd0a4
--- /dev/null
+++ b/users/Profpatsch/netencode/README.md
@@ -0,0 +1,133 @@
+# 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,]`
+
+## parser security considerations
+
+The length field is a decimal number that is not length-restricted,
+meaning an attacker could give an infinitely long length (or extremely long)
+thus overflowing your parser if you are not careful.
+
+You should thus put a practical length limit to the length of length fields,
+which implicitely enforces a length limit on how long the value itself can be.
+
+Start by defining a max value length in bytes.
+Then count the number of decimals in that number.
+
+So if your max length is 1024 bytes, your length field can be a maximum `count_digits(1024) == 4` bytes long.
+
+Thus, if you restrict your parser to a length field of 4 bytes,
+it should also never parse anything longer than 1024 bytes for the value
+(plus 1 byte for the type tag, 4 bytes for the length, and 2 bytes for the separator & ending character).
+
+## 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 000000000000..cb3dfaee4529
--- /dev/null
+++ b/users/Profpatsch/netencode/default.nix
@@ -0,0 +1,182 @@
+{ 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);
+
+  netencode-hs = pkgs.haskellPackages.mkDerivation {
+    pname = "netencode";
+    version = "0.1.0";
+
+    src = depot.users.Profpatsch.exactSource ./. [
+      ./netencode.cabal
+      ./Netencode.hs
+      ./Netencode/Parse.hs
+    ];
+
+    libraryHaskellDepends = [
+      pkgs.haskellPackages.hedgehog
+      pkgs.haskellPackages.nonempty-containers
+      pkgs.haskellPackages.deriving-compat
+      pkgs.haskellPackages.data-fix
+      pkgs.haskellPackages.bytestring
+      pkgs.haskellPackages.attoparsec
+      depot.users.Profpatsch.my-prelude
+    ];
+
+    isLibrary = true;
+    license = lib.licenses.mit;
+
+
+  };
+
+  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 t = netencode::t_from_stdin_or_die_user_error("netencode-pretty");
+      match netencode_pretty::Pretty::from_u(t.to_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
+      ];
+    } ''
+    extern crate netencode;
+    extern crate exec_helpers;
+    use netencode::{encode, dec};
+    use netencode::dec::{Decoder, DecodeError};
+
+    fn main() {
+        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 t = netencode::t_from_stdin_or_die_user_error("record-get");
+        match (dec::RecordDot {field, inner: dec::AnyU }).dec(t.to_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 t = netencode::t_from_stdin_or_die_user_error("record-splice-env");
+        let (_, prog) = exec_helpers::args_for_exec("record-splice-env", 0);
+        match Record(Try(ScalarAsBytes)).dec(t.to_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
+    netencode-hs
+    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 000000000000..efc9629ca0df
--- /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 000000000000..73ed5be1ded2
--- /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.cabal b/users/Profpatsch/netencode/netencode.cabal
new file mode 100644
index 000000000000..4e418d6dd887
--- /dev/null
+++ b/users/Profpatsch/netencode/netencode.cabal
@@ -0,0 +1,25 @@
+cabal-version:      2.4
+name:               netencode
+version:            0.1.0.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+library
+    exposed-modules:
+        Netencode,
+        Netencode.Parse
+
+    build-depends:
+        base >=4.15 && <5,
+        hedgehog,
+        nonempty-containers,
+        deriving-compat,
+        my-prelude,
+        data-fix,
+        bytestring,
+        attoparsec,
+        text,
+        semigroupoids,
+        selective
+
+    default-language: Haskell2010
diff --git a/users/Profpatsch/netencode/netencode.rs b/users/Profpatsch/netencode/netencode.rs
new file mode 100644
index 000000000000..34a8fcef0990
--- /dev/null
+++ b/users/Profpatsch/netencode/netencode.rs
@@ -0,0 +1,969 @@
+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 t_from_stdin_or_die_user_error<'a>(prog_name: &'_ str) -> T {
+    match t_from_stdin_or_die_user_error_with_rest(prog_name, &vec![]) {
+        None => exec_helpers::die_user_error(prog_name, "stdin was empty"),
+        Some((rest, t)) => {
+            if rest.is_empty() {
+                t
+            } else {
+                exec_helpers::die_user_error(
+                    prog_name,
+                    format!(
+                        "stdin contained some soup after netencode value: {:?}",
+                        String::from_utf8_lossy(&rest)
+                    ),
+                )
+            }
+        }
+    }
+}
+
+/// Read a netencode value from stdin incrementally, return bytes that could not be read.
+/// Nothing if there was nothing to read from stdin & no initial_bytes were provided.
+/// These can be passed back as `initial_bytes` if more values should be read.
+pub fn t_from_stdin_or_die_user_error_with_rest<'a>(
+    prog_name: &'_ str,
+    initial_bytes: &[u8],
+) -> Option<(Vec<u8>, T)> {
+    let mut chonker = Chunkyboi::new(std::io::stdin().lock(), 4096);
+    // The vec to pass to the parser on each step
+    let mut parser_vec: Vec<u8> = initial_bytes.to_vec();
+    // whether stdin was already empty
+    let mut was_empty: bool = false;
+    loop {
+        match chonker.next() {
+            None => {
+                if parser_vec.is_empty() {
+                    return None;
+                } else {
+                    was_empty = true
+                }
+            }
+            Some(Err(err)) => exec_helpers::die_temporary(
+                prog_name,
+                &format!("could not read from stdin: {:?}", err),
+            ),
+            Some(Ok(mut new_bytes)) => parser_vec.append(&mut new_bytes),
+        }
+
+        match parse::t_t(&parser_vec) {
+            Ok((rest, t)) => return Some((rest.to_owned(), t)),
+            Err(nom::Err::Incomplete(Needed)) => {
+                if was_empty {
+                    exec_helpers::die_user_error(
+                        prog_name,
+                        &format!(
+                            "unable to parse netencode from stdin, input incomplete: {:?}",
+                            parser_vec
+                        ),
+                    );
+                }
+                // read more from stdin and try parsing again
+                continue;
+            }
+            Err(err) => exec_helpers::die_user_error(
+                prog_name,
+                &format!("unable to parse netencode from stdin: {:?}", err),
+            ),
+        }
+    }
+}
+
+// iter helper
+// TODO: put into its own module
+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)),
+        }
+    }
+}
+
+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 000000000000..935c3d4a8a17
--- /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 000000000000..b8daea11d158
--- /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 000000000000..047fe6bae14d
--- /dev/null
+++ b/users/Profpatsch/netstring/default.nix
@@ -0,0 +1,64 @@
+{ lib, pkgs, depot, ... }:
+let
+  toNetstring = depot.nix.netstring.fromString;
+
+  toNetstringList = xs:
+    lib.concatStrings (map toNetstring xs);
+
+  toNetstringKeyVal = depot.nix.netstring.attrsToKeyValList;
+
+  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 000000000000..6a1062988f1e
--- /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/README.md b/users/Profpatsch/nix-home/README.md
new file mode 100644
index 000000000000..222978bc8cab
--- /dev/null
+++ b/users/Profpatsch/nix-home/README.md
@@ -0,0 +1,7 @@
+# nix-home
+
+My very much simplified version of [home-manager](https://github.com/nix-community/home-manager/).
+
+Only takes care about installing symlinks into `$HOME`, and uses [`GNU stow`](https://www.gnu.org/software/stow/) for doing the actual mutating.
+
+No support for services (yet).
diff --git a/users/Profpatsch/nix-home/default.nix b/users/Profpatsch/nix-home/default.nix
new file mode 100644
index 000000000000..ee154c549a6b
--- /dev/null
+++ b/users/Profpatsch/nix-home/default.nix
@@ -0,0 +1,212 @@
+{ 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;
+            }
+            {
+              name = "scripts/lorri-wait-for-eval";
+              path = depot.users.Profpatsch.lorri-wait-for-eval;
+            }
+            {
+              name = "scripts/lw";
+              path = depot.users.Profpatsch.lorri-wait-for-eval;
+            }
+
+          ]
+          ++
+          (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/read-http.nix b/users/Profpatsch/read-http.nix
new file mode 100644
index 000000000000..d9ad6fc30d94
--- /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 000000000000..2b24e6beb10a
--- /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
+// TODO: put into its own module
+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/README.md b/users/Profpatsch/reverse-haskell-deps/README.md
new file mode 100644
index 000000000000..efc288cae4a9
--- /dev/null
+++ b/users/Profpatsch/reverse-haskell-deps/README.md
@@ -0,0 +1,3 @@
+# reverse-haskell-deps
+
+Parse the HTML at `https://packdeps.haskellers.com/reverse` to get the data about Haskell package reverse dependencies in a structured way (they should just expose that as a json tbh).
diff --git a/users/Profpatsch/reverse-haskell-deps/ReverseHaskellDeps.hs b/users/Profpatsch/reverse-haskell-deps/ReverseHaskellDeps.hs
new file mode 100644
index 000000000000..0e18ce8a6b37
--- /dev/null
+++ b/users/Profpatsch/reverse-haskell-deps/ReverseHaskellDeps.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiWayIf #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Main where
+
+import Data.ByteString qualified as ByteString
+import Data.Either
+import Data.List qualified as List
+import Data.Maybe
+import Data.Text (Text)
+import Data.Text qualified as Text
+import Data.Text.Encoding qualified
+import MyPrelude
+import Numeric.Natural
+import Text.HTML.TagSoup qualified as Tag
+import Text.Nicify
+import Text.Read qualified as Read
+
+parseNat :: Text -> Maybe Natural
+parseNat = Read.readMaybe . textToString
+
+printNice :: Show a => a -> IO ()
+printNice = putStrLn . nicify . show
+
+type Tag = Tag.Tag Text
+
+main = do
+  reverseHtml <- readStdinUtf8
+  printNice $ List.sortOn snd $ packagesAndReverseDeps reverseHtml
+  where
+    readStdinUtf8 = bytesToTextUtf8Lenient <$> 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 :: Text -> [(Text, Natural)]
+packagesAndReverseDeps reverseHtml = do
+  let tags = Tag.parseTags reverseHtml
+  let sections = Tag.partitions (isJust . reverseLink) tags
+  let sectionName [] = "<unknown section>"
+      sectionName (sect : _) = sect & reverseLink & fromMaybe "<unknown section>"
+  let sectionNames = map sectionName sections
+  mapMaybe
+    ( \(name :: Text, sect) -> do
+        reverseDeps <- firstNaturalNumber sect
+        pure (sectionPackageName name sect, reverseDeps) :: Maybe (Text, Natural)
+    )
+    $ zip sectionNames sections
+  where
+    reverseLink = \case
+      Tag.TagOpen "a" attrs -> findMaybe 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 =
+      findMaybe
+        ( \case
+            Tag.TagText t -> parseNat t
+            _ -> Nothing
+        )
diff --git a/users/Profpatsch/reverse-haskell-deps/default.nix b/users/Profpatsch/reverse-haskell-deps/default.nix
new file mode 100644
index 000000000000..b0a44420d793
--- /dev/null
+++ b/users/Profpatsch/reverse-haskell-deps/default.nix
@@ -0,0 +1,32 @@
+{ 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 = [
+        depot.users.Profpatsch.my-prelude
+        pkgs.haskellPackages.nicify-lib
+        pkgs.haskellPackages.tagsoup
+      ];
+      ghcArgs = [ "-threaded" ];
+    }
+    ./ReverseHaskellDeps.hs;
+
+
+in
+rev
diff --git a/users/Profpatsch/reverse-haskell-deps/reverse-haskell-deps.cabal b/users/Profpatsch/reverse-haskell-deps/reverse-haskell-deps.cabal
new file mode 100644
index 000000000000..d8dd5cbe5a6e
--- /dev/null
+++ b/users/Profpatsch/reverse-haskell-deps/reverse-haskell-deps.cabal
@@ -0,0 +1,16 @@
+cabal-version:      2.4
+name:               reverse-haskell-deps
+version:            0.1.0.0
+author:             Profpatsch
+maintainer:         mail@profpatsch.de
+
+library
+    exposed-modules:          ReverseHaskellDeps.hs
+
+    build-depends:
+        base >=4.15 && <5,
+        my-prelude,
+        tagsoup,
+        nicify-lib
+
+    default-language: Haskell2010
diff --git a/users/Profpatsch/shell.nix b/users/Profpatsch/shell.nix
new file mode 100644
index 000000000000..3420bcdb0ace
--- /dev/null
+++ b/users/Profpatsch/shell.nix
@@ -0,0 +1,72 @@
+# generic shell.nix that can be used for most of my projects here,
+# until I figure out a way to have composable shells.
+let root = (import ../../. { }); in
+{ pkgs ? root.third_party.nixpkgs, depot ? root, ... }:
+
+pkgs.mkShell {
+  buildInputs = [
+    pkgs.sqlite-interactive
+    pkgs.sqlite-utils
+    pkgs.haskell-language-server
+    pkgs.cabal-install
+    (pkgs.haskellPackages.ghcWithHoogle (h: [
+      h.async
+      h.aeson-better-errors
+      h.conduit-extra
+      h.error
+      h.PyF
+      h.unliftio
+      h.wai
+      h.warp
+      h.profunctors
+      h.semigroupoids
+      h.validation-selective
+      h.free
+      h.cryptonite-conduit
+      h.sqlite-simple
+      h.hedgehog
+      h.http-conduit
+      h.nonempty-containers
+      h.deriving-compat
+      h.unix
+      h.tagsoup
+      h.attoparsec
+      h.iCalendar
+      h.case-insensitive
+      h.hscolour
+      h.nicify-lib
+      h.hspec-expectations-pretty-diff
+      depot.users.Profpatsch.my-prelude
+      depot.users.Profpatsch.netencode.netencode-hs
+      depot.users.Profpatsch.arglib.netencode.haskell
+      depot.users.Profpatsch.execline.exec-helpers-hs
+
+    ]))
+
+    pkgs.rustup
+    pkgs.pkg-config
+    pkgs.fuse
+  ];
+
+
+  RUSTC_WRAPPER =
+    let
+      wrapperArgFile = libs: pkgs.writeText "rustc-wrapper-args"
+        (pkgs.lib.concatStringsSep
+          "\n"
+          (pkgs.lib.concatLists
+            (map
+              (lib: [
+                "-L"
+                "${pkgs.lib.getLib lib}/lib"
+              ])
+              libs)));
+    in
+    depot.nix.writeExecline "rustc-wrapper" { readNArgs = 1; } [
+      "$1"
+      "$@"
+      "@${wrapperArgFile [
+      depot.third_party.rust-crates.nom
+    ]}"
+    ];
+}
diff --git a/users/Profpatsch/sync-abfall-ics-aichach-friedberg/README.md b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/README.md
new file mode 100644
index 000000000000..e0a6aa2fb83b
--- /dev/null
+++ b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/README.md
@@ -0,0 +1,3 @@
+# sync-abfall-ics-aichach-friedberg
+
+A small tool to sync the ICS files for the local trash collection times at https://abfallwirtschaft.lra-aic-fdb.de/
diff --git a/users/Profpatsch/sync-abfall-ics-aichach-friedberg/default.nix b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/default.nix
new file mode 100644
index 000000000000..739274cb6f1b
--- /dev/null
+++ b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/default.nix
@@ -0,0 +1,31 @@
+{ depot, pkgs, ... }:
+
+let
+  sync-to-dir = depot.users.Profpatsch.writers.python3
+    {
+      name = "sync-ics-to-dir";
+      libraries = (py: [
+        py.httpx
+        py.icalendar
+      ]);
+    } ./sync-ics-to-dir.py;
+
+  config =
+    depot.users.Profpatsch.importDhall.importDhall
+      {
+        root = ./..;
+        files = [
+          "sync-abfall-ics-aichach-friedberg/ics-to-caldav.dhall"
+          "dhall/lib.dhall"
+          "ini/ini.dhall"
+        ];
+        main = "sync-abfall-ics-aichach-friedberg/ics-to-caldav.dhall";
+        deps = [
+        ];
+      }
+      depot.users.Profpatsch.ini.externs;
+
+
+
+in
+{ inherit config; }
diff --git a/users/Profpatsch/sync-abfall-ics-aichach-friedberg/ics-to-caldav.dhall b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/ics-to-caldav.dhall
new file mode 100644
index 000000000000..2a7ac84979d2
--- /dev/null
+++ b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/ics-to-caldav.dhall
@@ -0,0 +1,139 @@
+let Ini = ../ini/ini.dhall
+
+let Lib = ../dhall/lib.dhall
+
+in  \(Ini/externs : Ini.Externs) ->
+      let Vdirsyncer =
+            let StorageType =
+                  < FileSystem : { path : Text, fileext : < ICS > }
+                  | Http : { url : Text }
+                  >
+
+            let Collection = < FromA | FromB | Collection : Text >
+
+            let Collections =
+                  < Unspecified | TheseCollections : List Collection >
+
+            let Storage = { storageName : Text, storage : StorageType }
+
+            in  { Storage
+                , StorageType
+                , Collection
+                , Collections
+                , Pair =
+                    { pairName : Text
+                    , a : Storage
+                    , b : Storage
+                    , collections : Collections
+                    }
+                }
+
+      let toIniSections
+          : Vdirsyncer.Pair -> Ini.Sections
+          = \(pair : Vdirsyncer.Pair) ->
+              let
+                  -- we assume the names are [a-zA-Z_]
+                  renderList =
+                    \(l : List Text) ->
+                          "["
+                      ++  Lib.Text/concatMapSep
+                            ", "
+                            Text
+                            (\(t : Text) -> "\"${t}\"")
+                            l
+                      ++  "]"
+
+              in  let nv = \(name : Text) -> \(value : Text) -> { name, value }
+
+                  let mkStorage =
+                        \(storage : Vdirsyncer.Storage) ->
+                          { name = "storage ${storage.storageName}"
+                          , value =
+                              merge
+                                { FileSystem =
+                                    \ ( fs
+                                      : { path : Text, fileext : < ICS > }
+                                      ) ->
+                                      [ nv "type" "filesystem"
+                                      , nv
+                                          "fileext"
+                                          (merge { ICS = ".ics" } fs.fileext)
+                                      , nv "path" fs.path
+                                      ]
+                                , Http =
+                                    \(http : { url : Text }) ->
+                                      [ nv "type" "http", nv "url" http.url ]
+                                }
+                                storage.storage
+                          }
+
+                  in  [ { name = "pair ${pair.pairName}"
+                        , value =
+                          [ nv "a" pair.a.storageName
+                          , nv "b" pair.b.storageName
+                          , nv
+                              "collections"
+                              ( merge
+                                  { Unspecified = "none"
+                                  , TheseCollections =
+                                      \(colls : List Vdirsyncer.Collection) ->
+                                        renderList
+                                          ( Lib.List/map
+                                              Vdirsyncer.Collection
+                                              Text
+                                              ( \ ( coll
+                                                  : Vdirsyncer.Collection
+                                                  ) ->
+                                                  merge
+                                                    { FromA = "from a"
+                                                    , FromB = "from b"
+                                                    , Collection =
+                                                        \(t : Text) -> t
+                                                    }
+                                                    coll
+                                              )
+                                              colls
+                                          )
+                                  }
+                                  pair.collections
+                              )
+                          ]
+                        }
+                      , mkStorage pair.a
+                      , mkStorage pair.b
+                      ]
+
+      in  { example =
+              Ini/externs.renderIni
+                ( Ini.appendInis
+                    ( Lib.List/map
+                        Vdirsyncer.Pair
+                        Ini.Ini
+                        ( \(pair : Vdirsyncer.Pair) ->
+                            { globalSection = [] : Ini.Section
+                            , sections = toIniSections pair
+                            }
+                        )
+                        (   [ { pairName = "testPair"
+                              , a =
+                                { storageName = "mystor"
+                                , storage =
+                                    Vdirsyncer.StorageType.FileSystem
+                                      { path = "./test-ics"
+                                      , fileext = < ICS >.ICS
+                                      }
+                                }
+                              , b =
+                                { storageName = "mystor"
+                                , storage =
+                                    Vdirsyncer.StorageType.Http
+                                      { url = "https://profpatsch.de" }
+                                }
+                              , collections = Vdirsyncer.Collections.Unspecified
+                              }
+                            ]
+                          : List Vdirsyncer.Pair
+                        )
+                    )
+                )
+          }
diff --git a/users/Profpatsch/sync-abfall-ics-aichach-friedberg/sync-ics-to-dir.py b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/sync-ics-to-dir.py
new file mode 100644
index 000000000000..4af3b9fb85ab
--- /dev/null
+++ b/users/Profpatsch/sync-abfall-ics-aichach-friedberg/sync-ics-to-dir.py
@@ -0,0 +1,133 @@
+# horrible little module that fetches ICS files for the local trash public service.
+#
+# It tries its best to not overwrite existing ICS files in case the upstream goes down
+# or returns empty ICS files.
+import sys
+import httpx
+import asyncio
+import icalendar
+from datetime import datetime
+import syslog
+import os.path
+
+# Internal id for the street (extracted from the ics download url)
+ortsteil_id = "e9c32ab3-df25-4660-b88e-abda91897d7a"
+
+# They are using a numeric encoding to refer to different kinds of trash
+fraktionen = {
+    "restmüll": "1",
+    "bio": "5",
+    "papier": "7",
+    "gelbe_tonne": "13",
+    "problemmüllsammlung": "20"
+}
+
+def ics_url(year):
+  frakt = ','.join(fraktionen.values())
+  return f'https://awido.cubefour.de/Customer/aic-fdb/KalenderICS.aspx?oid={ortsteil_id}&jahr={year}&fraktionen={frakt}&reminder=1.12:00'
+
+def fetchers_for_years(start_year, no_of_years_in_future):
+    """given a starting year, and a number of years in the future,
+    return the years for which to fetch ics files"""
+    current_year = datetime.now().year
+    max_year = current_year + no_of_years_in_future
+    return {
+        "passed_years": range(start_year, current_year),
+        "this_and_future_years": range(current_year, 1 + max_year)
+    }
+
+async def fetch_ics(c, url):
+    """fetch an ICS file from an URL"""
+    try:
+        resp = await c.get(url)
+    except Exception as e:
+        return { "ics_does_not_exist_exc": e }
+
+    if resp.is_error:
+        return { "ics_does_not_exist": resp }
+    else:
+        try:
+            ics = icalendar.Calendar.from_ical(resp.content)
+            return { "ics": { "ics_parsed": ics, "ics_bytes": resp.content } }
+        except ValueError as e:
+            return { "ics_cannot_be_parsed": e }
+
+def ics_has_events(ics):
+    """Determine if there is any event in the ICS, otherwise we can assume it’s an empty file"""
+    for item in ics.walk():
+      if isinstance(item, icalendar.Event):
+        return True
+    return False
+
+async def write_nonempty_ics(directory, year, ics):
+    # only overwrite if the new ics has any events
+    if ics_has_events(ics['ics_parsed']):
+        path = os.path.join(directory, f"{year}.ics")
+        with open(path, "wb") as f:
+            f.write(ics['ics_bytes'])
+            info(f"wrote ics for year {year} to file {path}")
+    else:
+        info(f"ics for year {year} was empty, skipping")
+
+
+def main():
+    ics_directory = os.getenv("ICS_DIRECTORY", None)
+    if not ics_directory:
+        critical("please set ICS_DIRECTORY")
+    start_year = int(os.getenv("ICS_START_YEAR", 2022))
+    future_years = int(os.getenv("ICS_FUTURE_YEARS", 2))
+
+    years = fetchers_for_years(start_year, no_of_years_in_future=future_years)
+
+
+    async def go():
+        async with httpx.AsyncClient(follow_redirects=True) as c:
+            info(f"fetching ics for passed years: {years['passed_years']}")
+            for year in years["passed_years"]:
+                match await fetch_ics(c, ics_url(year)):
+                    case { "ics_does_not_exist_exc": error }:
+                       warn(f"The ics for the year {year} is gone, error when requesting: {error} for url {ics_url(year)}")
+                    case { "ics_does_not_exist": resp }:
+                       warn(f"The ics for the year {year} is gone, server returned status {resp.status} for url {ics_url(year)}")
+                    case { "ics_cannot_be_parsed": error }:
+                       warn(f"The returned ICS could not be parsed: {error} for url {ics_url(year)}")
+                    case { "ics": ics }:
+                       info(f"fetched ics from {ics_url(year)}")
+                       await write_nonempty_ics(ics_directory, year, ics)
+                    case _:
+                       critical("unknown case for ics result")
+
+
+            info(f"fetching ics for current and upcoming years: {years['this_and_future_years']}")
+            for year in years["this_and_future_years"]:
+                match await fetch_ics(c, ics_url(year)):
+                    case { "ics_does_not_exist_exc": error }:
+                       critical(f"The ics for the year {year} is not available, error when requesting: {error} for url {ics_url(year)}")
+                    case { "ics_does_not_exist": resp }:
+                       critical(f"The ics for the year {year} is not available, server returned status {resp.status} for url {ics_url(year)}")
+                    case { "ics_cannot_be_parsed": error }:
+                       critical(f"The returned ICS could not be parsed: {error} for url {ics_url(year)}")
+                    case { "ics": ics }:
+                       info(f"fetched ics from {ics_url(year)}")
+                       await write_nonempty_ics(ics_directory, year, ics)
+                    case _:
+                       critical("unknown case for ics result")
+
+    asyncio.run(go())
+
+def info(msg):
+    syslog.syslog(syslog.LOG_INFO, msg)
+
+def critical(msg):
+    syslog.syslog(syslog.LOG_CRIT, msg)
+    sys.exit(1)
+
+def warn(msg):
+    syslog.syslog(syslog.LOG_WARNING, msg)
+
+def debug(msg):
+    syslog.syslog(syslog.LOG_DEBUG, msg)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/users/Profpatsch/tagtime/README.md b/users/Profpatsch/tagtime/README.md
new file mode 100644
index 000000000000..ab2c7d14e590
--- /dev/null
+++ b/users/Profpatsch/tagtime/README.md
@@ -0,0 +1,18 @@
+# tagtime reimplementation
+
+What’s great about original perl tagtime?
+
+* timestamps are deterministic from the beginning (keep)
+* the tagging system should just work (tm)
+
+What’s the problem with the original perl tagtime?
+
+* it uses a bad, arbitrary file format -> sqlite3
+* the query window does not time out, so it’s easy to miss that it’s open (often hidden behind another window), and then the following pings might never appear)
+* There’s a bug with tags containing a `.` -> sqlite3
+
+What would be cool to have?
+
+* multi-entry mode (ping on phone and laptop and merge the replies eventually since they will apply to single timestamps)
+* simplifying reporting based on fuzzy matching & history
+* auto-generate nice time reports with hours for work items
diff --git a/users/Profpatsch/toINI.nix b/users/Profpatsch/toINI.nix
new file mode 100644
index 000000000000..537505d30bbc
--- /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 000000000000..2224da2a3b8c
--- /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 000000000000..9fb69231a143
--- /dev/null
+++ b/users/Profpatsch/writers/default.nix
@@ -0,0 +1,120 @@
+{ 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) ];
+
+  pythonPackages = pkgs.python310Packages;
+  buildPythonPackages = pkgs.buildPackages.python310Packages;
+  python = pythonPackages.python;
+
+  python3 =
+    { name
+    , libraries ? (_: [ ])
+    , flakeIgnore ? [ ]
+    }:
+    let
+    in
+    pkgs.writers.makePythonWriter python pythonPackages buildPythonPackages name {
+      libraries = Libraries libraries pythonPackages;
+      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"
+            # … if there’s too many of them
+            "E303"
+            # or lines that are too long
+            "E501"
+          ];
+        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
+    pythonPackages.buildPythonPackage {
+      inherit name;
+      src = srcTree;
+      propagatedBuildInputs = libraries pythonPackages;
+      doCheck = false;
+    };
+
+
+  ghcBins = libraries: depot.nix.getBins (pkgs.ghc.withPackages (_: libraries)) [ "runghc" ];
+
+  writeHaskellInteractive = name: { libraries, ghcArgs ? [ ] }: path:
+    depot.nix.writeExecline name { } ([
+      (ghcBins libraries).runghc
+      "--"
+    ] ++ ghcArgs ++ [
+      "--"
+      path
+    ]);
+
+in
+{
+  inherit
+    python3
+    python3Lib
+    writeHaskellInteractive
+    ;
+}
diff --git a/users/Profpatsch/writers/tests/default.nix b/users/Profpatsch/writers/tests/default.nix
new file mode 100644
index 000000000000..879aae82f7a2
--- /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/README.md b/users/Profpatsch/ytextr/README.md
new file mode 100644
index 000000000000..f1e40d8e689c
--- /dev/null
+++ b/users/Profpatsch/ytextr/README.md
@@ -0,0 +1,5 @@
+# ytextr
+
+Wrapper around `yt-dlp` for downloading videos in good default quality with good default settings.
+
+Will always download the most up-to-date `yt-dlp` first, because the software usually stops working after a few weeks and needs to be updated, so just using `<nixpkgs>` often fails.
diff --git a/users/Profpatsch/ytextr/create-symlink-farm.nix b/users/Profpatsch/ytextr/create-symlink-farm.nix
new file mode 100644
index 000000000000..7b3a45b91681
--- /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 000000000000..ac630603b90c
--- /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/aaqaishtyaq/OWNERS b/users/aaqaishtyaq/OWNERS
new file mode 100644
index 000000000000..99c4a7424467
--- /dev/null
+++ b/users/aaqaishtyaq/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+aaqaishtyaq
diff --git a/users/cynthia/OWNERS b/users/cynthia/OWNERS
new file mode 100644
index 000000000000..762a4ec9ea2e
--- /dev/null
+++ b/users/cynthia/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+cynthia
diff --git a/users/cynthia/keys.nix b/users/cynthia/keys.nix
new file mode 100644
index 000000000000..e2f4ce488c9e
--- /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 000000000000..c0d1024f142c
--- /dev/null
+++ b/users/edef/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+edef
diff --git a/users/edef/depot-scan/default.nix b/users/edef/depot-scan/default.nix
new file mode 100644
index 000000000000..a9c0f382ff45
--- /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 000000000000..8808e2eb0023
--- /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 000000000000..77362b3f61f9
--- /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 000000000000..53e88c9e7345
--- /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/edef/refscan/.gitignore b/users/edef/refscan/.gitignore
new file mode 100644
index 000000000000..ee4b088aee93
--- /dev/null
+++ b/users/edef/refscan/.gitignore
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: edef <edef@edef.eu>
+# SPDX-License-Identifier: CC0-1.0
+
+/target
+**/*.rs.bk
diff --git a/users/edef/refscan/Cargo.lock b/users/edef/refscan/Cargo.lock
new file mode 100644
index 000000000000..a3515a75d720
--- /dev/null
+++ b/users/edef/refscan/Cargo.lock
@@ -0,0 +1,7 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "refscan"
+version = "0.1.0"
diff --git a/users/edef/refscan/Cargo.lock.license b/users/edef/refscan/Cargo.lock.license
new file mode 100644
index 000000000000..2cd6276751c0
--- /dev/null
+++ b/users/edef/refscan/Cargo.lock.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: edef <edef@edef.eu>
+SPDX-License-Identifier: CC0-1.0
+
diff --git a/users/edef/refscan/Cargo.toml b/users/edef/refscan/Cargo.toml
new file mode 100644
index 000000000000..dfac9a899ec9
--- /dev/null
+++ b/users/edef/refscan/Cargo.toml
@@ -0,0 +1,10 @@
+# SPDX-FileCopyrightText: edef <edef@edef.eu>
+# SPDX-License-Identifier: MPL-2.0
+
+[package]
+name = "refscan"
+version = "0.1.0"
+authors = ["edef <edef@edef.eu>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/users/edef/refscan/LICENSES/CC0-1.0.txt b/users/edef/refscan/LICENSES/CC0-1.0.txt
new file mode 100644
index 000000000000..0e259d42c996
--- /dev/null
+++ b/users/edef/refscan/LICENSES/CC0-1.0.txt
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+    CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+    LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+    ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+    INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+    REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+    PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+    THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+    HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+  i. the right to reproduce, adapt, distribute, perform, display,
+     communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+     likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+     subject to the limitations in paragraph 4(a), below;
+  v. rights protecting the extraction, dissemination, use and reuse of data
+     in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+     European Parliament and of the Council of 11 March 1996 on the legal
+     protection of databases, and under any national implementation
+     thereof, including any amended or successor version of such
+     directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+     world based on applicable law or treaty, and any national
+     implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+    surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+    warranties of any kind concerning the Work, express, implied,
+    statutory or otherwise, including without limitation warranties of
+    title, merchantability, fitness for a particular purpose, non
+    infringement, or the absence of latent or other defects, accuracy, or
+    the present or absence of errors, whether or not discoverable, all to
+    the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+    that may apply to the Work or any use thereof, including without
+    limitation any person's Copyright and Related Rights in the Work.
+    Further, Affirmer disclaims responsibility for obtaining any necessary
+    consents, permissions or other rights required for any use of the
+    Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+    party to this document and has no duty or obligation with respect to
+    this CC0 or use of the Work.
diff --git a/users/edef/refscan/LICENSES/MPL-2.0.txt b/users/edef/refscan/LICENSES/MPL-2.0.txt
new file mode 100644
index 000000000000..ee6256cdb62a
--- /dev/null
+++ b/users/edef/refscan/LICENSES/MPL-2.0.txt
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in 
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/users/edef/refscan/src/lib.rs b/users/edef/refscan/src/lib.rs
new file mode 100644
index 000000000000..3d4a07f3dd1c
--- /dev/null
+++ b/users/edef/refscan/src/lib.rs
@@ -0,0 +1,154 @@
+// SPDX-FileCopyrightText: edef <edef@edef.eu>
+// SPDX-License-Identifier: MPL-2.0
+
+use self::simd::u8x32;
+
+fn prefilter(haystack: u8x32) -> u32 {
+    let alp = haystack.gt(u8x32::splat(b'a' - 1)) & haystack.lt(u8x32::splat(b'z' + 1));
+    let num = haystack.gt(u8x32::splat(b'0' - 1)) & haystack.lt(u8x32::splat(b'9' + 1));
+    alp | num
+}
+
+/// scan_clean returns `Err(&buffer[..n])` of known pointer-free data,
+/// or `Ok(buffer)` if the entire buffer is pointer-free.
+pub fn scan_clean(buffer: &[u8]) -> Result<&[u8], &[u8]> {
+    let buffer = {
+        let n = buffer.len() & !31;
+        &buffer[..n]
+    };
+
+    let mut masks = buffer
+        .chunks_exact(32)
+        .map(|chunk| prefilter(u8x32::from_slice_unaligned(chunk)))
+        .enumerate()
+        .map(|e| (e.0 * 32, e.1))
+        .peekable();
+
+    while let Some((offset, mask)) = masks.next() {
+        let peek = masks.peek().map(|x| x.1).unwrap_or(!0 >> 1);
+        let n = (!mask).leading_zeros() + (!peek).trailing_zeros();
+        if n >= 32 {
+            let offset = offset + mask.trailing_zeros() as usize;
+            return Err(&buffer[..offset]);
+        }
+    }
+
+    Ok(buffer)
+}
+
+#[cfg(test)]
+mod test {
+    #[test]
+    fn scan_tail() {
+        let buffer = b"_xfbmj7sl2ikicym9x3yq7cms5qx1w39k";
+        assert_eq!(crate::scan_clean(buffer), Err(&buffer[..1]));
+    }
+    #[test]
+    fn scan_straddle() {
+        let buffer = b"________________xfbmj7sl2ikicym9x3yq7cms5qx1w39k________________";
+        assert_eq!(crate::scan_clean(buffer), Err(&buffer[..16]));
+    }
+    #[test]
+    fn scan_clean() {
+        let buffer = b"x_______________xfbmj7sl2ikicym9x3yq-cms5qx1w3-k________________";
+        assert_eq!(crate::scan_clean(buffer), Ok(&buffer[..]));
+    }
+}
+
+#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
+mod simd {
+    #[cfg(target_arch = "x86")]
+    use std::arch::x86 as arch;
+    #[cfg(target_arch = "x86_64")]
+    use std::arch::x86_64 as arch;
+    use {
+        arch::{__m256i, _mm256_cmpgt_epi8, _mm256_movemask_epi8, _mm256_set1_epi8},
+        std::ptr,
+    };
+
+    #[allow(non_camel_case_types)]
+    #[derive(Copy, Clone)]
+    pub struct u8x32(__m256i);
+
+    impl u8x32 {
+        #[inline(always)]
+        pub fn from_slice_unaligned(slice: &[u8]) -> Self {
+            assert_eq!(slice.len(), 32);
+            u8x32(unsafe { ptr::read_unaligned(slice.as_ptr().cast()) })
+        }
+
+        #[inline(always)]
+        pub fn splat(x: u8) -> Self {
+            u8x32(unsafe { _mm256_set1_epi8(x as i8) })
+        }
+
+        #[inline(always)]
+        pub fn gt(self, b: Self) -> u32 {
+            unsafe { _mm256_movemask_epi8(_mm256_cmpgt_epi8(self.0, b.0)) as u32 }
+        }
+
+        #[inline(always)]
+        pub fn lt(self, b: Self) -> u32 {
+            b.gt(self)
+        }
+    }
+}
+
+#[cfg(target_arch = "aarch64")]
+mod simd {
+    use std::{
+        arch::aarch64::{
+            uint8x16_t as u8x16, vaddv_u8, vandq_u8, vcgtq_u8, vdupq_n_u8, vget_high_u8,
+            vget_low_u8, vshlq_u8,
+        },
+        mem, ptr,
+    };
+
+    #[allow(non_camel_case_types)]
+    #[derive(Copy, Clone)]
+    #[repr(transparent)]
+    pub struct u8x32([u8x16; 2]);
+
+    impl u8x32 {
+        #[cfg(target_endian = "little")]
+        #[inline(always)]
+        pub fn from_slice_unaligned(slice: &[u8]) -> Self {
+            assert_eq!(slice.len(), 32);
+            u8x32(unsafe { ptr::read_unaligned(slice.as_ptr().cast()) })
+        }
+
+        #[inline(always)]
+        pub fn splat(x: u8) -> Self {
+            u8x32(unsafe {
+                let x = vdupq_n_u8(x);
+                [x, x]
+            })
+        }
+
+        #[inline(always)]
+        pub fn gt(&self, b: Self) -> u32 {
+            let u8x32([al, ah]) = *self;
+            let u8x32([bl, bh]) = b;
+
+            fn f(a: u8x16, b: u8x16) -> u32 {
+                unsafe {
+                    let c = vshlq_u8(
+                        vandq_u8(vdupq_n_u8(0x80), vcgtq_u8(a, b)),
+                        mem::transmute([
+                            -7, -6, -5, -4, -3, -2, -1, 0, -7, -6, -5, -4, -3, -2, -1, 0i8,
+                        ]),
+                    );
+
+                    (vaddv_u8(vget_low_u8(c)) as u32) << 0 | (vaddv_u8(vget_high_u8(c)) as u32) << 8
+                }
+            }
+
+            f(al, bl) << 0 | f(ah, bh) << 16
+        }
+
+        #[inline(always)]
+        pub fn lt(self, b: Self) -> u32 {
+            b.gt(self)
+        }
+    }
+}
diff --git a/users/edef/refscan/src/main.rs b/users/edef/refscan/src/main.rs
new file mode 100644
index 000000000000..e572abf0a1ee
--- /dev/null
+++ b/users/edef/refscan/src/main.rs
@@ -0,0 +1,58 @@
+// SPDX-FileCopyrightText: edef <edef@edef.eu>
+// SPDX-License-Identifier: MPL-2.0
+
+use std::{
+    collections::BTreeSet as Set,
+    convert::TryInto,
+    io::{self, Read},
+    str,
+};
+
+fn main() {
+    let max_refs: Set<[u8; 32]> = include_str!("../testdata/maxrefs")
+        .lines()
+        .map(|l| l.as_bytes().try_into().unwrap())
+        .collect();
+
+    let input = {
+        let stdin = io::stdin();
+        let mut buffer = Vec::new();
+        stdin.lock().read_to_end(&mut buffer).unwrap();
+        buffer
+    };
+
+    let base = input.as_ptr() as usize;
+    let mut input: &[u8] = &input;
+    while input.len() >= 32 {
+        match refscan::scan_clean(&input) {
+            Ok(buffer) | Err(buffer) => {
+                let n = buffer.len();
+                input = &input[n..];
+            }
+        }
+
+        let buffer = {
+            let idx = input.iter().position(|x| match x {
+                b'a'..=b'z' | b'0'..=b'9' => false,
+                _ => true,
+            });
+            idx.map(|idx| &input[..idx]).unwrap_or(input)
+        };
+
+        for chunk in buffer.windows(32) {
+            let offset = (chunk.as_ptr() as usize) - base;
+            let chunk = {
+                let mut fixed = [0u8; 32];
+                fixed.copy_from_slice(chunk);
+                fixed
+            };
+            if max_refs.contains(&chunk) {
+                let seen = unsafe { str::from_utf8_unchecked(&chunk) };
+                println!("{} {}", seen, offset);
+            }
+        }
+
+        let n = buffer.len();
+        input = &input[n..];
+    }
+}
diff --git a/users/edef/refscan/testdata/.gitignore b/users/edef/refscan/testdata/.gitignore
new file mode 100644
index 000000000000..1d278bd6ce97
--- /dev/null
+++ b/users/edef/refscan/testdata/.gitignore
@@ -0,0 +1,6 @@
+# SPDX-FileCopyrightText: edef <edef@edef.eu>
+# SPDX-License-Identifier: CC0-1.0
+
+/maxrefs
+/nar
+/result
diff --git a/users/edef/refscan/testdata/generate.sh b/users/edef/refscan/testdata/generate.sh
new file mode 100755
index 000000000000..9f416024c181
--- /dev/null
+++ b/users/edef/refscan/testdata/generate.sh
@@ -0,0 +1,8 @@
+#! /usr/bin/env bash
+# SPDX-FileCopyrightText: edef <edef@edef.eu>
+# SPDX-License-Identifier: CC0-1.0
+set -euo pipefail
+
+drv=$(nix-instantiate '<nixpkgs>' -A ghc)
+nix --extra-experimental-features nix-command show-derivation -r "$drv" | jq -r '.[] | .outputs[].path, .inputSrcs[]' | sort -u | cut -d/ -f4 | cut -d- -f1 > maxrefs
+nix-store --dump "$(nix-build "$drv")" > nar
diff --git a/users/ericvolp12/OWNERS b/users/ericvolp12/OWNERS
new file mode 100644
index 000000000000..a43910023c34
--- /dev/null
+++ b/users/ericvolp12/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+ericvolp12
diff --git a/users/eta/OWNERS b/users/eta/OWNERS
new file mode 100644
index 000000000000..59e5e2120a7f
--- /dev/null
+++ b/users/eta/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+eta
diff --git a/users/eta/keys.nix b/users/eta/keys.nix
new file mode 100644
index 000000000000..ebdc8479a5a8
--- /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 000000000000..1b7dd7189f9e
--- /dev/null
+++ b/users/firefly/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+firefly
diff --git a/users/firefly/keys.nix b/users/firefly/keys.nix
new file mode 100644
index 000000000000..1d7467a0747c
--- /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 000000000000..a70f5eefffd5
--- /dev/null
+++ b/users/flokli/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+flokli
diff --git a/users/flokli/keys.nix b/users/flokli/keys.nix
new file mode 100644
index 000000000000..790c9862f824
--- /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 000000000000..0095f0e9506c
--- /dev/null
+++ b/users/grfn/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+grfn
diff --git a/users/grfn/achilles/.envrc b/users/grfn/achilles/.envrc
new file mode 100644
index 000000000000..b80e28b4b815
--- /dev/null
+++ b/users/grfn/achilles/.envrc
@@ -0,0 +1,2 @@
+source_up
+eval "$(lorri direnv)"
diff --git a/users/grfn/achilles/.gitignore b/users/grfn/achilles/.gitignore
new file mode 100644
index 000000000000..ea8c4bf7f35f
--- /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 000000000000..3c767db1e4d7
--- /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 000000000000..f091399a0dd4
--- /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 000000000000..ac5296ebbd74
--- /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 000000000000..3a8cd2865e87
--- /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 000000000000..faf8ce90e353
--- /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 000000000000..dc6e7a1f3e34
--- /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 000000000000..20f1677235c0
--- /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 000000000000..70635d978c7c
--- /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 000000000000..714be6072881
--- /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
+    pkg-config
+  ]);
+
+  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 000000000000..1434cf8a32c2
--- /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 000000000000..cdfaef567d7a
--- /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 000000000000..5438d29d2cf7
--- /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 000000000000..9a71ac954e00
--- /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 000000000000..8ef057dba04f
--- /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 000000000000..0bea482c1478
--- /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 000000000000..be8767575ab5
--- /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 000000000000..efd7399ed1c4
--- /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 000000000000..fd0a822708c2
--- /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 000000000000..59a5e46c466f
--- /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 000000000000..51575a895e91
--- /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 000000000000..8368a6dd180f
--- /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 000000000000..016e9f6ed99a
--- /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 000000000000..45b215473d7f
--- /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 000000000000..268d6f479a1e
--- /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 000000000000..70df7a0724a5
--- /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 000000000000..272d1167a33c
--- /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 000000000000..5ae1b59b3a8e
--- /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 000000000000..b18ce4a0dc88
--- /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 000000000000..406e5c0e699e
--- /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 000000000000..e088cbca10a5
--- /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 000000000000..b80f0e0860a1
--- /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 000000000000..bb53fb7fff50
--- /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 000000000000..872c449eb020
--- /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 000000000000..251a988f4f6f
--- /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 000000000000..85ee1cce4859
--- /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 000000000000..306869bef1d5
--- /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 000000000000..5825bab1fbe9
--- /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 000000000000..0f1086bfd8e1
--- /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 000000000000..8faddb77ecc4
--- /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 000000000000..051d09d292a8
--- /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 000000000000..99dbfc443636
--- /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 000000000000..fc45477984e3
--- /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 000000000000..a7181333b96f
--- /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 000000000000..c8a6dd156d2c
--- /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 000000000000..c6d603be2a99
--- /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 000000000000..6afb68353c44
--- /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 000000000000..39ce843c22b5
--- /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 000000000000..02f5ecb4683c
--- /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 000000000000..c30e328ffa24
--- /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 000000000000..7aa21978bbe7
--- /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 000000000000..46e8cd67e318
--- /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 000000000000..b81118ed6b32
--- /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 000000000000..352147a6d0fd
--- /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 000000000000..8554f3d0ed0b
--- /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 000000000000..70bb2c77e4cf
--- /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 000000000000..012c0985f12f
--- /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 000000000000..c86c3f24f009
--- /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 000000000000..69b818a4f4ab
--- /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 000000000000..9718d84748ae
--- /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 000000000000..936abf6c7d19
--- /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 000000000000..5e82dcb1711c
--- /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 000000000000..cbee0c00acd9
--- /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 000000000000..5895cad56bdf
--- /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 000000000000..1c83d8518d3d
--- /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 000000000000..9dc5c7f158af
--- /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 000000000000..71476d858fd5
--- /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 000000000000..0fb9838c9d76
--- /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 000000000000..e7f8a31ba35c
--- /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 000000000000..29cc1a973450
--- /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 000000000000..79203dd7801d
--- /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 000000000000..0abb707aeddf
--- /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 000000000000..65415571a7f4
--- /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 000000000000..674e6eabe747
--- /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 000000000000..67f1e85379c2
--- /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 000000000000..469aede09c6b
--- /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 000000000000..676a065e24ff
--- /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 000000000000..70788c273207
--- /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 000000000000..87c0b64d0a37
--- /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 000000000000..1f53798bb4fe
--- /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 000000000000..e26569657f07
--- /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 000000000000..49a6d621de66
--- /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 000000000000..f34c41198e66
--- /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 000000000000..632774d5cdac
--- /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 000000000000..5bbf88925aa1
--- /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 000000000000..da5ee29321fb
--- /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 000000000000..492f786bd660
--- /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 000000000000..1b5a4e11ecd7
--- /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 000000000000..31411e5d4504
--- /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 000000000000..700105ef6350
--- /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 000000000000..e854ec1d147d
--- /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 000000000000..35bc580e3933
--- /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 000000000000..aa0578f3546b
--- /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 000000000000..7b6b4c27648b
--- /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 000000000000..d7307c40673b
--- /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 000000000000..ce84b88e97c1
--- /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 000000000000..caa679ee873f
--- /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 000000000000..6f6d6f3585ae
--- /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 000000000000..17d48755365c
--- /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 000000000000..ed1d7644f539
--- /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 000000000000..d13d63e16cc2
--- /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 000000000000..945d681c6f82
--- /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 000000000000..a860ae607626
--- /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 000000000000..f48c8d73388e
--- /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 000000000000..d458aa5592d2
--- /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 000000000000..88f1b50caaa8
--- /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 000000000000..40716632a3c9
--- /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 000000000000..89ac92669914
--- /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 000000000000..988959fd0603
--- /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 000000000000..0278f89f5edd
--- /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 000000000000..a44b21d4cb24
--- /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 000000000000..f9755577a570
--- /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 000000000000..d7d698a58c8d
--- /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 000000000000..d5b19d9ebc88
--- /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 000000000000..35945fd415e2
--- /dev/null
+++ b/users/grfn/emacs.d/+bindings.el
@@ -0,0 +1,1432 @@
+;; -*- 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)
+(evil-set-command-property 'flycheck-previous-error :repeat nil)
+(evil-set-command-property 'smerge-next :repeat nil)
+(evil-set-command-property 'smerge-prev :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
+     "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 000000000000..518f185cb9c2
--- /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 000000000000..6273c67d6ecd
--- /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 000000000000..1fd0e3988771
--- /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 000000000000..319c93c05e47
--- /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 000000000000..87b2236d12c7
--- /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 000000000000..be4161441dce
--- /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 000000000000..f001a3e12b68
--- /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 000000000000..e623aa2de131
--- /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 000000000000..da2ef48cd8fd
--- /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
+        )
+
+  (transient-define-suffix magit-commit-wip ()
+    (interactive)
+    (magit-commit-create '("-m" "wip")))
+
+  (transient-append-suffix
+    #'magit-commit
+    ["c"]
+    (list "W" "Commit WIP" #'magit-commit-wip))
+
+  (transient-define-suffix magit-reset-head-back ()
+    (interactive)
+    (magit-reset-mixed "HEAD~"))
+
+  (transient-define-suffix 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 000000000000..5b5dc8ead652
--- /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 000000000000..70360d00727c
--- /dev/null
+++ b/users/grfn/emacs.d/email.el
@@ -0,0 +1,53 @@
+;;; -*- 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)
+
+(defun apply-thread-patchset (repo branch)
+  (interactive "Dgit repo: \nsnew branch name: ")
+  (let ((tid notmuch-show-thread-id)
+        (tmp "/tmp/notmuch-patchset"))
+    (shell-command (format "notmuch-extract-patch %s > %s && ( cd %s && git checkout -b %s && git am %s )"
+                           (shell-quote-argument tid)
+                           (shell-quote-argument tmp)
+                           (shell-quote-argument (expand-file-name repo))
+                           (shell-quote-argument branch)
+                           (shell-quote-argument tmp)))))
diff --git a/users/grfn/emacs.d/github-org.el b/users/grfn/emacs.d/github-org.el
new file mode 100644
index 000000000000..f4f9d2e37069
--- /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 000000000000..9bb12c61aae4
--- /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 000000000000..75776a38cd9d
--- /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 000000000000..a3672e0a0e0e
--- /dev/null
+++ b/users/grfn/emacs.d/init.el
@@ -0,0 +1,173 @@
+;;; -*- 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
+       ;;god               ; run Emacs commands without modifier keys
+       ;;lispy             ; vim for lisp, for people who dont like vim
+       ;;multiple-cursors  ; editing in many places at once
+       ;;objed             ; text object editing for the innocent
+       ;;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]
+       electric          ; smarter, keyword-based electric-indent
+       ;;eshell            ; a consistent, cross-platform shell (WIP)
+       ;;term              ; terminals in Emacs
+       (undo +tree)
+       vc                ; version-control and Emacs, sitting in a 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
+       tree-sitter       ; syntax and parsing, sitting in a tree...
+       ;;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 +tree-sitter)              ; 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
+       zig
+
+       ;; 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 000000000000..117869599d83
--- /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 000000000000..c45cc7e6e381
--- /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 000000000000..16f6252d8b27
--- /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 000000000000..ec5b474af218
--- /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 000000000000..8e6c3e0417ff
--- /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 000000000000..2ff8612f838c
--- /dev/null
+++ b/users/grfn/emacs.d/org-config.el
@@ -0,0 +1,197 @@
+;;; -*- lexical-binding: t; -*-
+
+(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)
+
+(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)
+
+   ("w" "Work Todo" entry
+    (file+headline ,(notes-file "work.org") "Work Tasks")
+    "* TODO %?\n%i")
+
+   ("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)
+
+(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 000000000000..3e315c5e6046
--- /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 000000000000..e403c9e56fd3
--- /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 000000000000..15a3843f4db9
--- /dev/null
+++ b/users/grfn/emacs.d/packages.el
@@ -0,0 +1,154 @@
+;; -*- 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! jiralib2)
+(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 000000000000..9988d16a5327
--- /dev/null
+++ b/users/grfn/emacs.d/rust.el
@@ -0,0 +1,42 @@
+;;; -*- lexical-binding: t; -*-
+
+(add-to-list 'auto-mode-alist '("\\.rs$" . rust-mode))
+
+(defun grfn/rust-setup ()
+  (interactive)
+
+  (direnv--maybe-update-environment)
+
+  (+evil-embrace-angle-bracket-modes-hook-h)
+
+  ;; (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"]
+        rustic-format-trigger 'on-save
+        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 000000000000..ab65a912a8d1
--- /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 000000000000..b5bd4db7482c
--- /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 000000000000..54d3b40b099c
--- /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 000000000000..8a2854d759df
--- /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 000000000000..cbb1646e41d1
--- /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 000000000000..fdd8250d86ca
--- /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 000000000000..68863f70542b
--- /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 000000000000..bf39a2a3eecb
--- /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 000000000000..f25a9b8d409e
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/hlint
@@ -0,0 +1,8 @@
+# -*- mode: snippet -*-
+# name: hlint
+# uuid: hlint
+# 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 000000000000..4a7fca2c2fd6
--- /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 000000000000..6e17b83d7114
--- /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 000000000000..1beafbe50b56
--- /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 000000000000..6f84720f4511
--- /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 000000000000..b22ea3d2e888
--- /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 000000000000..4554d33f9ba7
--- /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 000000000000..fccff1d66f29
--- /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 000000000000..bf6a66f8a34f
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/haskell-mode/test-group
@@ -0,0 +1,9 @@
+# -*- mode: snippet -*-
+# name: test-group
+# uuid: test-group
+# 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 000000000000..036b0ae9983a
--- /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 000000000000..7bcd99b5716c
--- /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 000000000000..ef8d1a3863ee
--- /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 000000000000..4569b6583143
--- /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 000000000000..d83809f3c35e
--- /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 000000000000..bd0198181d02
--- /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 000000000000..eba41ef3309d
--- /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 000000000000..b423044b4410
--- /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 000000000000..3e303764cb0b
--- /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 000000000000..a451cfc08a90
--- /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 000000000000..00da312e1096
--- /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 000000000000..dc79819d8979
--- /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 000000000000..0bb0f024367b
--- /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 000000000000..938d490a74e8
--- /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 000000000000..d2447e4b5a4d
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/nix-mode/fetchFromGitHub
@@ -0,0 +1,12 @@
+# -*- mode: snippet -*-
+# name: fetchFromGitHub
+# uuid: fetchFromGitHub
+# 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 000000000000..0a74c21e1857
--- /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 000000000000..bc640e5ab09b
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/nix-mode/sha256
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: sha256
+# uuid: sha256
+# 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 000000000000..b5d43fd6bc01
--- /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 000000000000..b4db0f433aec
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/org-mode/combat
@@ -0,0 +1,13 @@
+# -*- mode: snippet -*-
+# name: combat
+# uuid: combat
+# 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 000000000000..297529cdac64
--- /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 000000000000..fde469276c3f
--- /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 000000000000..a43bc95cc3ed
--- /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 000000000000..e5be85d6b3c0
--- /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 000000000000..247ae51b0b78
--- /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 000000000000..1bdbdfa5dc36
--- /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 000000000000..37f2dd31caff
--- /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 000000000000..75fd19b6414b
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/prolog-mode/use-module
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: use-module
+# uuid: use-module
+# 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 000000000000..47e83850d5b7
--- /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 000000000000..4f96748572a2
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/decorate
@@ -0,0 +1,15 @@
+# -*- mode: snippet -*-
+# name: decorate
+# uuid: decorate
+# 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 000000000000..71d99dddc67d
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/dunder
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: dunder
+# uuid: dunder
+# 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 000000000000..1495cc91d9fb
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/name
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: name
+# uuid: name
+# 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 000000000000..aba801c6baf9
--- /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 000000000000..41c6f87cbfc1
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/python-mode/pdb
@@ -0,0 +1,7 @@
+# -*- mode: snippet -*-
+# name: pdb
+# uuid: pdb
+# 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 000000000000..fea942a337f6
--- /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 000000000000..2352d7b56bcc
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/async test
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# name: async test
+# uuid: atest
+# 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 000000000000..9ec43075380b
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/benchmark
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# name: benchmark
+# uuid: benchmark
+# 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 000000000000..be12af49113a
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/proptest
@@ -0,0 +1,10 @@
+# -*- mode: snippet -*-
+# name: proptest
+# uuid: proptest
+# 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/test-module b/users/grfn/emacs.d/snippets/rust-mode/test-module
new file mode 100644
index 000000000000..bfa2ca2d1881
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/rust-mode/test-module
@@ -0,0 +1,11 @@
+# -*- mode: snippet -*-
+# name: test-module
+# uuid: test-module
+# key: tmod
+# condition: t
+# --
+mod $1 {
+    use super::*;
+
+    $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 000000000000..0a476ab58661
--- /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 000000000000..d38ffceafbad
--- /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 000000000000..6acc46ff397a
--- /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 000000000000..14822f1a05a8
--- /dev/null
+++ b/users/grfn/emacs.d/snippets/terraform-mode/variable
@@ -0,0 +1,11 @@
+# -*- mode: snippet -*-
+# name: variable
+# uuid: variable
+# 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 000000000000..7b9431147011
--- /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 000000000000..dbc9704d7909
--- /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 000000000000..eef397a24ea6
--- /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 000000000000..2d69c9bad9db
--- /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 000000000000..6495a1a5952e
--- /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 000000000000..ae00b6b5fc75
--- /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 000000000000..21192753a268
--- /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 000000000000..a7fdea46da32
--- /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 000000000000..be81feddb1a5
--- /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 000000000000..2b72eaed2988
--- /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 000000000000..d6c9f40c9536
--- /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 000000000000..b05d897d3ddb
--- /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 000000000000..5ab3614d7902
--- /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 000000000000..668751de6754
--- /dev/null
+++ b/users/grfn/gws.fyi/index.org
@@ -0,0 +1,41 @@
+#+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 (they/them)  and i'm a software engineer and musician.
+
+you might know me from elsewhere as grfn, glittershark, or gws
+
+* 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 000000000000..cdcd440766ca
--- /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 000000000000..aac4e32e7ac5
--- /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 000000000000..74e9b103c670
--- /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 000000000000..846bdb6677a3
--- /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 000000000000..057c4d3ee698
--- /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 000000000000..b2be92b7db01
--- /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 000000000000..b085883a1049
--- /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 000000000000..929ec7d6289e
--- /dev/null
+++ b/users/grfn/keyboard/default.nix
@@ -0,0 +1,73 @@
+{ 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"
+      # GCC 12 has improved array-bounds warnings, failing the build of QMK.
+      # Newer versions of the firmware would work probably, but they heavily
+      # altered the build system, so it is non-trivial. Backporting the patch
+      # that fixes it seems difficult – the next change to the offending matrix.c
+      # after the pinned qmkSource commit is
+      # https://github.com/qmk/qmk_firmware/commit/11c308d436180974b7719ce78cdffdd83a1302c0
+      # which heavily changes the way the code works.
+      #
+      # TODO(grfn): address this properly
+      "-Wno-error=array-bounds"
+    ];
+
+    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 000000000000..76def36f9ca8
--- /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 000000000000..316c435fed6c
--- /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 000000000000..741b7b2cfd51
--- /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_LSFT,        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 000000000000..29d5a3fa631b
--- /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 000000000000..2a7dd97debf1
--- /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 000000000000..f15e387d5464
--- /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 000000000000..1777f0fac3ea
--- /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 000000000000..9cd8fbe8921d
--- /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 000000000000..e6e29b575187
--- /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/pkgs/cargo-hakari.nix b/users/grfn/pkgs/cargo-hakari.nix
new file mode 100644
index 000000000000..b6f4e7e40007
--- /dev/null
+++ b/users/grfn/pkgs/cargo-hakari.nix
@@ -0,0 +1,27 @@
+{ pkgs, ... }:
+
+with pkgs;
+
+rustPlatform.buildRustPackage rec {
+  pname = "cargo-hakari";
+  version = "0.9.13";
+
+  src = fetchFromGitHub {
+    owner = "facebookincubator";
+    repo = "cargo-guppy";
+    rev = "cargo-hakari-${version}";
+    sha256 = "11ds2zryxdd6rvszkpphb0xnfg7rqisg6kixrwyiydjrm5rdjg9d";
+  };
+
+  cargoSha256 = "0b2hjyak5v4m3g5zjk2q8bdb4iv3015qw1rmhpclv4cv48lcmdbb";
+
+  buildAndTestSubdir = "tools/cargo-hakari";
+
+  nativeBuildInputs = [
+    pkg-config
+  ];
+
+  buildInputs = [
+    openssl
+  ];
+}
diff --git a/users/grfn/pkgs/cargo-nextest.nix b/users/grfn/pkgs/cargo-nextest.nix
new file mode 100644
index 000000000000..dbf3bd7eef19
--- /dev/null
+++ b/users/grfn/pkgs/cargo-nextest.nix
@@ -0,0 +1,27 @@
+{ pkgs, ... }:
+
+with pkgs;
+
+rustPlatform.buildRustPackage rec {
+  pname = "cargo-nextest";
+  version = "0.9.36";
+
+  src = fetchFromGitHub {
+    owner = "nextest-rs";
+    repo = "nextest";
+    rev = "cargo-nextest-${version}";
+    sha256 = "1g40r38bqmdhc0dy07pj27vkc64d3fw6v5z2vwn82xld2h9dg7w2";
+  };
+
+  cargoSha256 = "1g862azgkn3xk3v3chs8hv1b1prj1pq2vfzbhcx6ir9l00kv6gcv";
+
+  cargoTestFlags = [
+    "--"
+    "--skip"
+    "tests_integration::test_relocated_run"
+    "--skip"
+    "tests_integration::test_run"
+    "--skip"
+    "tests_integration::test_run_after_build"
+  ];
+}
diff --git a/users/grfn/pkgs/notmuch-extract-patch.nix b/users/grfn/pkgs/notmuch-extract-patch.nix
new file mode 100644
index 000000000000..7f00f925ec88
--- /dev/null
+++ b/users/grfn/pkgs/notmuch-extract-patch.nix
@@ -0,0 +1,18 @@
+{ pkgs, ... }:
+
+let
+  inherit (pkgs) lib;
+  src = pkgs.fetchurl {
+    url = "https://raw.githubusercontent.com/aaptel/notmuch-extract-patch/f732a53e12a7c91a06755ebfab2007adc9b3063b/notmuch-extract-patch";
+    sha256 = "0nawkl04sj7psw6ikzay7kydj3dhd0fkwghcsf5rzaw4bmp4kbax";
+  };
+
+in
+pkgs.runCommand "notmuch-extract-patch"
+{
+  buildInputs = [ pkgs.python3 ];
+} ''
+  mkdir -p $out/bin
+  install -m 755 ${src} $out/bin/notmuch-extract-patch
+  patchShebangs $out/bin
+''
diff --git a/users/grfn/resume/chimera.png b/users/grfn/resume/chimera.png
new file mode 100644
index 000000000000..6dde989c53b0
--- /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 000000000000..4f1540a9d214
--- /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 000000000000..4454e74c8261
--- /dev/null
+++ b/users/grfn/resume/default.nix
@@ -0,0 +1,40 @@
+{ pkgs, ... }:
+
+with pkgs.lib;
+
+pkgs.runCommand "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 000000000000..dacc129a1025
--- /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 000000000000..a40f80733736
--- /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 000000000000..3a6e1477f322
--- /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 000000000000..7b949c704acd
--- /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 000000000000..4de7f848a04e
--- /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 000000000000..9018726a2384
--- /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 000000000000..134ae2401133
--- /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 000000000000..d3dc5345b080
--- /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 000000000000..681181997d38
--- /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 000000000000..1fc53f2180e1
--- /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 000000000000..0a4e2864be29
--- /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 000000000000..eb1b1ec727bb
--- /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 000000000000..fb0b70fdcd10
--- /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 000000000000..e375e7612a5a
--- /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 000000000000..63cf97aa3b7d
--- /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 000000000000..85932464d1c5
--- /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 000000000000..ff732f4e2af5
--- /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 000000000000..63b21b5320ed
--- /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 000000000000..933558d570b6
--- /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 000000000000..adc939893261
--- /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 000000000000..051d09d292a8
--- /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 000000000000..6c15dcdf7361
--- /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 000000000000..0ae5aa5502f7
--- /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 000000000000..9e9e370f1bec
--- /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 000000000000..e2f6e9360385
--- /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 000000000000..0de870710571
--- /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 000000000000..26b1998f565b
--- /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 000000000000..986ad181b87c
--- /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 000000000000..6e70458d1972
--- /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 000000000000..41fbeb02c47d
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..554ee0523e46
--- /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 000000000000..f821999b9957
--- /dev/null
+++ b/users/grfn/system/home/default.nix
@@ -0,0 +1,36 @@
+{ 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;
+
+  ogopogo = home ./machines/ogopogo.nix;
+
+  ogopogoHome = ogopogo.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 000000000000..39045c147d76
--- /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 000000000000..0b8503a00e98
--- /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/ogopogo.nix b/users/grfn/system/home/machines/ogopogo.nix
new file mode 100644
index 000000000000..763efa2e308a
--- /dev/null
+++ b/users/grfn/system/home/machines/ogopogo.nix
@@ -0,0 +1,75 @@
+{ pkgs, lib, config, ... }:
+
+let
+  inherit (builtins) pathExists;
+  laptopKeyboardId = "5";
+in
+
+{
+  imports = [
+    ../platforms/linux.nix
+    ../modules/common.nix
+    ../modules/desktop.nix
+    ../modules/development/agda.nix
+    ../modules/development/readyset.nix
+  ] ++ (lib.optional (pathExists ../modules/private.nix) ../modules/private.nix);
+
+  programs.home-manager.enable = true;
+  home.stateVersion = "21.11";
+
+  system.machine = {
+    wirelessInterface = "wlp4s0";
+    i3FontSize = 9;
+    battery = false;
+  };
+
+  home.packages = with pkgs; [
+    zoom-us
+    slack
+    mariadb
+    graphviz
+    gnuplot
+    mypaint
+    xdot
+    tdesktop
+    subsurface
+    (discord.override rec {
+      version = "0.0.22";
+      src = fetchurl {
+        url = "https://dl.discordapp.net/apps/linux/${version}/discord-${version}.tar.gz";
+        sha256 = "19xbmrd782m4lp2l0ww5v3ip227g0z8pplxigxga96q43rvp6p0p";
+      };
+    })
+    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";
+
+  # Telegram adds this to ~/.config/mimeapps.list if it isn't already there,
+  # preventing home manager from installing (since it doesn't want to overwrite
+  # the file)
+  xdg.mimeApps.defaultApplications."x-scheme-handler/tg" =
+    "userapp-Telegram Desktop-K290F1.desktop";
+}
diff --git a/users/grfn/system/home/machines/roswell.nix b/users/grfn/system/home/machines/roswell.nix
new file mode 100644
index 000000000000..135477b12ddf
--- /dev/null
+++ b/users/grfn/system/home/machines/roswell.nix
@@ -0,0 +1,63 @@
+{ pkgs, lib, config, ... }:
+
+let
+  inherit (builtins) pathExists;
+in
+
+{
+  imports = [
+    ../platforms/linux.nix
+    ../modules/shell.nix
+    ../modules/development.nix
+    ../modules/emacs.nix
+    ../modules/vim.nix
+    ../modules/development/readyset.nix
+    ../modules/tmux.nix
+  ] ++ (lib.optional (pathExists ../modules/private.nix) ../modules/private.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
+    nixpkgs-review
+    cachix
+
+    # ReadySet stuff
+    nodejs
+    mysql80
+
+    (writeShellScriptBin "xdg-open" "echo xdg-open: \"$@\"")
+  ];
+
+  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 000000000000..6ffa02a57c6a
--- /dev/null
+++ b/users/grfn/system/home/machines/yeren.nix
@@ -0,0 +1,76 @@
+{ pkgs, lib, config, ... }:
+
+let
+  inherit (builtins) pathExists;
+  laptopKeyboardId = "5";
+in
+
+{
+  imports = [
+    ../platforms/linux.nix
+    ../modules/common.nix
+    ../modules/desktop.nix
+    ../modules/development/agda.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
+    mariadb
+    graphviz
+    gnuplot
+    mypaint
+    xdot
+    tdesktop
+    subsurface
+    discord
+    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/.gitignore b/users/grfn/system/home/modules/.gitignore
new file mode 100644
index 000000000000..a211cae6c6ea
--- /dev/null
+++ b/users/grfn/system/home/modules/.gitignore
@@ -0,0 +1 @@
+private.nix
diff --git a/users/grfn/system/home/modules/alacritty.nix b/users/grfn/system/home/modules/alacritty.nix
new file mode 100644
index 000000000000..67d6638a31f8
--- /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 000000000000..204f9c8e1428
--- /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 000000000000..7714644841ba
--- /dev/null
+++ b/users/grfn/system/home/modules/common.nix
@@ -0,0 +1,125 @@
+{ config, lib, pkgs, ... }:
+
+# Everything in here needs to work on linux or darwin, with or without a desktop
+# environment
+
+{
+  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
+    nixpkgs-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
+      rm /tmp/mugwump
+    '')
+    (writeShellScriptBin "rebuild-roswell" ''
+      set -eo pipefail
+      cd ~/code/depot
+      nix build -f . users.grfn.system.system.roswellSystem -o /tmp/roswell
+      nix copy -f . users.grfn.system.system.roswellSystem \
+        --to ssh://roswell
+      system=$(readlink -ef /tmp/roswell)
+      ssh roswell sudo nix-env -p /nix/var/nix/profiles/system --set $system
+      ssh roswell sudo $system/bin/switch-to-configuration switch
+      rm /tmp/roswell
+    '')
+    (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";
+        forwardAgent = true;
+      };
+    };
+  };
+
+  programs.direnv = {
+    enable = true;
+    enableBashIntegration = true;
+    enableZshIntegration = true;
+  };
+
+  services.dropbox = {
+    enable = true;
+  };
+}
diff --git a/users/grfn/system/home/modules/desktop.nix b/users/grfn/system/home/modules/desktop.nix
new file mode 100644
index 000000000000..08277e9c594c
--- /dev/null
+++ b/users/grfn/system/home/modules/desktop.nix
@@ -0,0 +1,25 @@
+{ config, lib, pkgs, ... }:
+
+# Things that only work in the presence of a linux desktop environment
+
+{
+  imports = [
+    ./i3.nix
+    ./obs.nix
+    ./games.nix
+  ];
+
+  home.packages = with pkgs; [
+    ntfy
+  ];
+
+  services.syncthing.tray.enable = true;
+
+  gtk = {
+    enable = true;
+    gtk3.bookmarks = [
+      "file:///home/grfn/code"
+      "file:///home/grfn/notes"
+    ];
+  };
+}
diff --git a/users/grfn/system/home/modules/development.nix b/users/grfn/system/home/modules/development.nix
new file mode 100644
index 000000000000..75af94bd4002
--- /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";
+    };
+
+    vendorHash = "sha256: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 000000000000..afd22a306dc9
--- /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 000000000000..876b0c08df1d
--- /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 000000000000..9acc338e423a
--- /dev/null
+++ b/users/grfn/system/home/modules/development/readyset.nix
@@ -0,0 +1,34 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ./rust.nix
+  ];
+
+  home.packages = with pkgs; [
+    # These go in $PATH so I can run it from rofi and parent to my WM
+    (writeShellScriptBin "dotclip" "xclip -out -selection clipboard | dot -Tpng | feh -")
+    (writeShellScriptBin "dotcontroller" "curl -s localhost:6033/graph | dot -Tpng | feh -")
+
+    rain
+    awscli2
+    ssm-session-manager-plugin
+    amazon-ecr-credential-helper
+    postgresql_14
+  ];
+
+  programs.zsh.shellAliases = {
+    "tf" = "terraform";
+  };
+
+  home.file.".docker/config.json".text = builtins.toJSON {
+    credHelpers = {
+      "305232526136.dkr.ecr.us-east-2.amazonaws.com" = "ecr-login";
+      "public.ecr.aws" = "ecr-login";
+    };
+  };
+
+  programs.zsh.functions."purge_deployment" = ''
+    for key in $(http :8500/v1/kv/$1 keys==true | jq -r .'[]'); do http DELETE ":8500/v1/kv/$key"; done
+  '';
+}
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 000000000000..9ed42f5d2ddf
--- /dev/null
+++ b/users/grfn/system/home/modules/development/rust.nix
@@ -0,0 +1,45 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (config.lib) depot;
+in
+{
+
+  home.packages = with pkgs; [
+    rustup
+    cargo-edit
+    cargo-expand
+    cargo-rr
+    cargo-udeps
+    cargo-bloat
+    sccache
+    evcxr
+
+    depot.users.grfn.pkgs.cargo-hakari
+    depot.users.grfn.pkgs.cargo-nextest
+
+    # benchmarking+profiling
+    cargo-criterion
+    cargo-flamegraph
+    coz
+    inferno
+    hotspot
+  ];
+
+  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 000000000000..b84b36843781
--- /dev/null
+++ b/users/grfn/system/home/modules/emacs.nix
@@ -0,0 +1,108 @@
+{ 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
+            bussproofs
+            bussproofs-extra
+            ;
+        })
+
+        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;
+    })
+  ];
+}
diff --git a/users/grfn/system/home/modules/email.nix b/users/grfn/system/home/modules/email.nix
new file mode 100644
index 000000000000..55dd70529b9b
--- /dev/null
+++ b/users/grfn/system/home/modules/email.nix
@@ -0,0 +1,98 @@
+{ 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
+{
+  # 2022-09-26: workaround for home-manager defaulting to removed pkgs.gmailieer
+  # attribute, can likely be removed soon
+  programs.lieer.package = pkgs.lieer;
+
+  programs.lieer.enable = true;
+  programs.notmuch.enable = true;
+  services.lieer.enable = true;
+  programs.msmtp.enable = true;
+
+  home.packages = with pkgs; [
+    mu
+    msmtp
+    config.lib.depot.users.grfn.pkgs.notmuch-extract-patch
+  ];
+
+  systemd.user.services = mapAttrs'
+    (name: account: {
+      name = escapeUnitName "lieer-${name}";
+      value.Service = {
+        ExecStart = mkForce "${pkgs.writeShellScript "sync-${name}" ''
+        ${pkgs.lieer}/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 000000000000..c7e78685a5a3
--- /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 000000000000..5e2800389208
--- /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 = true;
+  });
+
+  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
+      prismlauncher
+    ];
+  })
+]
diff --git a/users/grfn/system/home/modules/i3.nix b/users/grfn/system/home/modules/i3.nix
new file mode 100644
index 000000000000..311ccd94b49a
--- /dev/null
+++ b/users/grfn/system/home/modules/i3.nix
@@ -0,0 +1,392 @@
+{ 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;
+      };
+
+      i3FontSize = mkOption {
+        description = "Font size to use in i3 window decorations etc.";
+        default = 6;
+        type = types.int;
+      };
+
+      battery = mkOption {
+        description = "Does this system have a battery?";
+        default = true;
+        type = types.bool;
+      };
+    };
+  };
+
+  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 -p spotify play-pause";
+                  "XF86AudioNext" = "exec playerctl -p spotify next";
+                  "XF86AudioPrev" = "exec playerctl -p spotify 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"
+                  ${lib.optionalString (config.system.machine.battery) ''
+                      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;
+              };
+            };
+          }];
+
+          window.titlebar = true;
+        };
+      };
+
+      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 000000000000..806a8be03cb2
--- /dev/null
+++ b/users/grfn/system/home/modules/lib/cloneRepo.nix
@@ -0,0 +1,76 @@
+{ lib, config, ... }:
+with lib;
+{
+  options = {
+    grfn.impure.clonedRepos = mkOption {
+      description = "Repositories to clone";
+      default = { };
+      type = with types; attrsOf (
+        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
+              if $DRY_RUN_CMD git clone "${repoURL}" "${path}"; then
+                pushd ${path}
+                $DRY_RUN_CMD ${onClone}
+                popd
+              else
+                echo "Git repository ${path} failed to clone"
+              fi
+            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 000000000000..228dc6379fd6
--- /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 000000000000..d4b16b44f0bf
--- /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 000000000000..7962320f8a2f
--- /dev/null
+++ b/users/grfn/system/home/modules/obs.nix
@@ -0,0 +1,18 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (pkgs) obs-studio;
+  obs-input-overlay = pkgs.obs-studio-plugins.input-overlay;
+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 000000000000..436c8f261797
--- /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 000000000000..666e28259c14
--- /dev/null
+++ b/users/grfn/system/home/modules/pure.zsh-theme
@@ -0,0 +1,155 @@
+#!/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() {
+	if [[ "$TERM" == "dumb" ]]; then
+		return
+	fi
+
+	# 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 000000000000..c8a404a1f49e
--- /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 000000000000..8d8d5884ce8d
--- /dev/null
+++ b/users/grfn/system/home/modules/shell.nix
@@ -0,0 +1,189 @@
+{ 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
+  ];
+
+  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";
+      };
+    }];
+
+    initExtraFirst = ''
+      if [[ "$TERM" = "dumb" ]]; then
+        return
+      fi
+    '';
+
+    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 = ''
+      if [[ "$TERM" != "dumb" ]]; then
+        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
+
+        export RPS1=""
+        autoload -U promptinit; promptinit
+        prompt pure
+      fi
+
+      if [[ "$TERM" == "dumb" ]]; then
+        unsetopt zle
+        unsetopt prompt_cr
+        unsetopt prompt_subst
+        unset zle_bracketed_paste
+        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 000000000000..87002610cbcf
--- /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/tmux.nix b/users/grfn/system/home/modules/tmux.nix
new file mode 100644
index 000000000000..adbaa02f32f9
--- /dev/null
+++ b/users/grfn/system/home/modules/tmux.nix
@@ -0,0 +1,42 @@
+{ config, lib, pkgs, ... }:
+
+{
+  programs.tmux = {
+    enable = true;
+    customPaneNavigationAndResize = true;
+    keyMode = "vi";
+    newSession = true;
+    prefix = "C-a";
+    shell = "${pkgs.zsh}/bin/zsh";
+    shortcut = "a";
+
+    extraConfig = ''
+      set -g status-bg "colour0"
+      set -g message-command-fg "colour7"
+      set -g status-justify "centre"
+      set -g status-left-length "100"
+      set -g status "on"
+      set -g pane-active-border-fg "colour14"
+      set -g message-bg "colour11"
+      set -g status-right-length "100"
+      set -g status-right-attr "none"
+      set -g message-fg "colour7"
+      set -g message-command-bg "colour11"
+      set -g status-attr "none"
+      # set -g status-utf8 "on"
+      set -g pane-border-fg "colour11"
+      set -g status-left-attr "none"
+      setw -g window-status-fg "colour10"
+      setw -g window-status-attr "none"
+      setw -g window-status-activity-bg "colour0"
+      setw -g window-status-activity-attr "none"
+      setw -g window-status-activity-fg "colour14"
+      setw -g window-status-separator ""
+      setw -g window-status-bg "colour0"
+      set -g status-left "#[fg=colour15,bg=colour14,bold] #S #[fg=colour14,bg=colour11,nobold,nounderscore,noitalics]#[fg=colour7,bg=colour11] #F #[fg=colour11,bg=colour0,nobold,nounderscore,noitalics]#[fg=colour10,bg=colour0] #W #[fg=colour0,bg=colour0,nobold,nounderscore,noitalics]"
+      set -g status-right "#{battery_status_bg} Batt: #{battery_percentage} #{battery_remain} | #[fg=colour0,bg=colour0,nobold,nounderscore,noitalics]#[fg=colour10,bg=colour0] %a #[fg=colour11,bg=colour0,nobold,nounderscore,noitalics]#[fg=colour7,bg=colour11] %b %d  %R #[fg=colour14,bg=colour11,nobold,nounderscore,noitalics]#[fg=colour15,bg=colour14] #H "
+      setw -g window-status-format "#[fg=colour0,bg=colour0,nobold,nounderscore,noitalics]#[default] #I  #W #[fg=colour0,bg=colour0,nobold,nounderscore,noitalics]"
+      setw -g window-status-current-format "#[fg=colour0,bg=colour11,nobold,nounderscore,noitalics]#[fg=colour7,bg=colour11] #I  #W #[fg=colour11,bg=colour0,nobold,nounderscore,noitalics]"
+    '';
+  };
+}
diff --git a/users/grfn/system/home/modules/twitter.nix b/users/grfn/system/home/modules/twitter.nix
new file mode 100644
index 000000000000..3cb2e90adc34
--- /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 000000000000..b87cb09ad125
--- /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 000000000000..3e33b5e2bee7
--- /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 000000000000..a12173d6842d
--- /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 000000000000..f98b80f26915
--- /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 000000000000..5429f3e094b3
--- /dev/null
+++ b/users/grfn/system/home/platforms/linux.nix
@@ -0,0 +1,84 @@
+{ 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/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
+    gimp # TODO(grfn): use glimpse once it build again
+
+    # System utilities
+    powertop
+    usbutils
+    pciutils
+    gdmap
+    lsof
+    tree
+    nmap
+    iftop
+
+    # Security
+    gnupg
+    keybase
+    openssl
+    yubikey-manager
+    # TODO(grfn): lagging behind yubikey-manager and doesn't support cryptography >= 39
+    # yubikey-manager-qt
+
+    # Spotify...etc
+    spotify
+    playerctl
+  ];
+
+  services.redshift = {
+    enable = true;
+    provider = "geoclue2";
+  };
+
+  services.pasystray.enable = true;
+
+  services.gpg-agent = {
+    enable = true;
+  };
+
+  programs.zsh.initExtra = ''
+    [[ ! $IN_NIX_SHELL && "$TERM" != "dumb" ]] && alsi -l
+  '';
+
+  services.lorri.enable = true;
+}
diff --git a/users/grfn/system/install b/users/grfn/system/install
new file mode 100755
index 000000000000..a9a45953da07
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..eae567015b73
--- /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 000000000000..98b0fcf18e3c
--- /dev/null
+++ b/users/grfn/system/system/default.nix
@@ -0,0 +1,46 @@
+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;
+
+  ogopogo = import ./machines/ogopogo.nix;
+
+  ogopogoSystem = (depot.ops.nixos.nixosFor ogopogo).system;
+
+  yeren = import ./machines/yeren.nix;
+
+  yerenSystem = (depot.ops.nixos.nixosFor yeren).system;
+
+  # TODO(grfn): reenable after
+  # https://github.com/NixOS/nixpkgs/pull/234883 has propagated
+  # through to our channel
+  # iso = import ./iso.nix args;
+
+  meta.ci.targets = [
+    "mugwumpSystem"
+    "roswellSystem"
+    "ogopogoSystem"
+    "yerenSystem"
+
+    "iso"
+  ];
+}
diff --git a/users/grfn/system/system/iso.nix b/users/grfn/system/system/iso.nix
new file mode 100644
index 000000000000..398145f1cfab
--- /dev/null
+++ b/users/grfn/system/system/iso.nix
@@ -0,0 +1,22 @@
+{ 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;
+
+    # TODO(grfn): enabling this (in the minimal profile) fails the iso build,
+    # since gtk+3 needs to be built which fails due to cairo without xlibs
+    environment.noXlibs = 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 000000000000..0fec21409255
--- /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 000000000000..138383484fb6
--- /dev/null
+++ b/users/grfn/system/system/machines/mugwump.nix
@@ -0,0 +1,293 @@
+{ 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")
+  ];
+
+  networking.hostName = "mugwump";
+
+  system.stateVersion = "22.05";
+
+  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
+    {
+      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;
+    settings = {
+      PasswordAuthentication = false;
+      PermitRootLogin = "no";
+    };
+  };
+
+  services.grafana = {
+    enable = true;
+    dataDir = "/var/lib/grafana";
+
+    settings = {
+      server = {
+        http_port = 3000;
+        root_url = "https://metrics.gws.fyi";
+        domain = "metrics.gws.fyi";
+      };
+      analytics.reporting_enabled = false;
+    };
+
+    provision = {
+      enable = true;
+      datasources.settings.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.settings.server.http_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;
+
+  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/ogopogo.nix b/users/grfn/system/system/machines/ogopogo.nix
new file mode 100644
index 000000000000..f51ffd285870
--- /dev/null
+++ b/users/grfn/system/system/machines/ogopogo.nix
@@ -0,0 +1,84 @@
+{ depot, modulesPath, config, lib, pkgs, ... }:
+
+{
+  imports = [
+    (modulesPath + "/installer/scan/not-detected.nix")
+    ../modules/common.nix
+    ../modules/xserver.nix
+    ../modules/fonts.nix
+    ../modules/sound.nix
+    ../modules/tvl.nix
+    ../modules/development.nix
+    ../modules/wireshark.nix
+    ../modules/work/kolide.nix
+  ];
+
+  networking.hostName = "ogopogo";
+
+  system.stateVersion = "22.11";
+
+  boot = {
+    initrd = {
+      availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
+      kernelModules = [ ];
+    };
+
+    kernelPackages = pkgs.linuxPackages_5_15;
+
+    kernelModules = [ "kvm-amd" ];
+    blacklistedKernelModules = [ ];
+    extraModulePackages = [ ];
+
+    kernel.sysctl = {
+      "kernel.perf_event_paranoid" = -1;
+    };
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "/dev/disk/by-uuid/d67506cf-7039-484d-97c0-00321a7858dc";
+      fsType = "ext4";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/AE73-03A3";
+      fsType = "vfat";
+    };
+  };
+
+  swapDevices = [{
+    device = "/dev/disk/by-uuid/8bdae7c8-5160-491f-8cd0-4f0a79acadf9";
+  }];
+
+  services.earlyoom = {
+    enable = true;
+    freeMemThreshold = 5;
+  };
+
+  nixpkgs.config.allowUnfree = true;
+  hardware.enableAllFirmware = 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;
+
+  hardware.keyboard.zsa.enable = true;
+
+  # Nvidia
+  services.xserver = {
+    videoDrivers = [ "nvidia" ];
+    dpi = 100;
+  };
+  hardware.opengl.enable = true;
+  services.picom = {
+    enable = true;
+    vSync = true;
+  };
+  hardware.opengl.driSupport32Bit = true;
+}
diff --git a/users/grfn/system/system/machines/roswell.nix b/users/grfn/system/system/machines/roswell.nix
new file mode 100644
index 000000000000..8287c95425ca
--- /dev/null
+++ b/users/grfn/system/system/machines/roswell.nix
@@ -0,0 +1,31 @@
+{ depot, config, lib, pkgs, modulesPath, ... }:
+
+{
+  imports = [
+    ../modules/common.nix
+    ../modules/development.nix
+    "${modulesPath}/installer/scan/not-detected.nix"
+    "${modulesPath}/virtualisation/amazon-image.nix"
+  ];
+
+  system.stateVersion = "22.05";
+
+  networking.hostName = "roswell";
+
+  users.users.grfn.openssh.authorizedKeys.keys = [
+    depot.users.grfn.keys.main
+  ];
+
+  boot.loader.systemd-boot.enable = lib.mkForce false;
+  boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
+
+  services.openssh.settings.PasswordAuthentication = false;
+
+  services.tailscale.enable = true;
+
+  security.sudo.wheelNeedsPassword = false;
+
+  environment.systemPackages = with pkgs; [
+    cloud-utils
+  ];
+}
diff --git a/users/grfn/system/system/machines/yeren.nix b/users/grfn/system/system/machines/yeren.nix
new file mode 100644
index 000000000000..f104debfc85d
--- /dev/null
+++ b/users/grfn/system/system/machines/yeren.nix
@@ -0,0 +1,135 @@
+{ 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";
+
+  time.timeZone = "America/New_York";
+
+  services.avahi = {
+    enable = true;
+    nssmdns = true;
+  };
+
+  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.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 000000000000..3edec8a5525e
--- /dev/null
+++ b/users/grfn/system/system/modules/common.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+let
+
+  depot = import ../../../../.. { };
+
+in
+
+with lib;
+
+{
+  boot = {
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+    tmp.cleanOnBoot = 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"
+    ];
+    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 000000000000..3adbd9d9b07f
--- /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 000000000000..d17e9d424c28
--- /dev/null
+++ b/users/grfn/system/system/modules/development.nix
@@ -0,0 +1,15 @@
+{ config, lib, pkgs, ... }:
+
+{
+  virtualisation.docker.enable = true;
+  users.users.grfn.extraGroups = [ "docker" ];
+
+  security.pam.loginLimits = [
+    {
+      domain = "grfn";
+      type = "soft";
+      item = "nofile";
+      value = "65535";
+    }
+  ];
+}
diff --git a/users/grfn/system/system/modules/fcitx.nix b/users/grfn/system/system/modules/fcitx.nix
new file mode 100644
index 000000000000..812f598f9f47
--- /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 000000000000..f30600b28b39
--- /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 000000000000..05c5333e513f
--- /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 000000000000..34d9bfdcb729
--- /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 000000000000..151c2a246f32
--- /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 000000000000..ce58ebb0dcda
--- /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 000000000000..07a67a1ec43b
--- /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 000000000000..fc189972da77
--- /dev/null
+++ b/users/grfn/system/system/modules/tvl.nix
@@ -0,0 +1,38 @@
+{ 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"
+      ];
+      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/wireshark.nix b/users/grfn/system/system/modules/wireshark.nix
new file mode 100644
index 000000000000..30658419fc93
--- /dev/null
+++ b/users/grfn/system/system/modules/wireshark.nix
@@ -0,0 +1,9 @@
+{ config, lib, pkgs, ... }:
+
+{
+  programs.wireshark = {
+    enable = true;
+    package = pkgs.wireshark;
+  };
+  users.users.grfn.extraGroups = [ "wireshark" ];
+}
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 000000000000..a319a5806fca
--- /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 000000000000..e4ee786f0cbe
--- /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 000000000000..35ee44112ea1
--- /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 000000000000..c6bc24c22b65
--- /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 000000000000..23cd83880420
--- /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 000000000000..114105642a3c
--- /dev/null
+++ b/users/grfn/terraform/workspace.nix
@@ -0,0 +1,107 @@
+{ pkgs, depot, ... }:
+name: { plugins }: module_tf:
+
+let
+
+  inherit (pkgs) lib runCommand 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 = runCommand "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 = runCommand "${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 = runCommand "${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 000000000000..4fd7f25fcf8c
--- /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 000000000000..217443e2df82
--- /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 000000000000..be81feddb1a5
--- /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 000000000000..cfe8e35df091
--- /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 000000000000..cb7aca541a3f
--- /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 000000000000..df82de3e8caf
--- /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 000000000000..2ad31c01d443
--- /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 000000000000..45644ff76449
--- /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 000000000000..7e1fedb069b1
--- /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 000000000000..9a994af677b0
--- /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 000000000000..c771a0d932cb
--- /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 000000000000..5889618ee432
--- /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 000000000000..c553abd6d5d0
--- /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 000000000000..56310e691c33
--- /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 000000000000..fae4af92a7a5
--- /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 000000000000..f0c936bfca18
--- /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 000000000000..748c65b3e0db
--- /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 000000000000..cd1dbe24d361
--- /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 000000000000..049c92fb4c9c
--- /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 000000000000..e5bcda04268f
--- /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 000000000000..e7cf01d158e5
--- /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 000000000000..7d7c16440545
--- /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 000000000000..15a36fe964be
--- /dev/null
+++ b/users/grfn/xanthous/package.yaml
@@ -0,0 +1,157 @@
+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
+- -fconstraint-solver-iterations=6 # Xanthous.Data, Xanthous.Command
+
+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 000000000000..f8364c467abe
--- /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 000000000000..051d09d292a8
--- /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 000000000000..2f7896d1d136
--- /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 000000000000..173298b158c1
--- /dev/null
+++ b/users/grfn/xanthous/server/Cargo.lock
@@ -0,0 +1,1874 @@
+# 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.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[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.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+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.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "bytes"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db"
+
+[[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.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+dependencies = [
+ "iana-time-zone",
+ "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.2.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "indexmap",
+ "once_cell",
+ "strsim",
+ "termcolor",
+ "textwrap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
+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 = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset",
+ "scopeguard",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
+dependencies = [
+ "cfg-if",
+]
+
+[[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.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
+dependencies = [
+ "crc32fast",
+ "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.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56"
+
+[[package]]
+name = "futures-task"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1"
+
+[[package]]
+name = "futures-util"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90"
+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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "gimli"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[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.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[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.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac"
+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 = "iana-time-zone"
+version = "0.1.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "indenter"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
+
+[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+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.134"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
+
+[[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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+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 0.11.2",
+ "indexmap",
+ "metrics",
+ "num_cpus",
+ "ordered-float",
+ "parking_lot",
+ "quanta",
+ "radix_trie",
+ "sketches-ddsketch",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
+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.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[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.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
+
+[[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.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[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.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "raw-cpuid"
+version = "10.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+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.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+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.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[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.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04d2ecae5fcf33b122e2e6bd520a57ccf152d2dde3b38c71039df1a6867264ee"
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+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.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+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.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
+dependencies = [
+ "once_cell",
+ "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-ident"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+
+[[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.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "web-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+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 000000000000..d4a064beb697
--- /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 000000000000..fbb5ccd26920
--- /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 000000000000..09054cb00fcf
--- /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 000000000000..82de6e38e1af
--- /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 000000000000..e01c0316a6b2
--- /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 000000000000..1b2c1c104b33
--- /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 000000000000..6912cdd9c9ee
--- /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 000000000000..234ecd8f2336
--- /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 000000000000..2c41cb4aa864
--- /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 000000000000..e89fcd621157
--- /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 000000000000..1f2b513ffe0e
--- /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 000000000000..426230cdc2fc
--- /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 000000000000..5d4db1a47465
--- /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 000000000000..69ba6f0e0596
--- /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 000000000000..f9ee9fe1686f
--- /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)
+  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 000000000000..cca352858d9c
--- /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 000000000000..6e6274a02c6f
--- /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 000000000000..1acd14a0640b
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Data.hs
@@ -0,0 +1,822 @@
+{-# 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 (Arg (Position' a) a, 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 via (GenericArbitrary Direction) instance Arbitrary 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 via (GenericArbitrary (Neighbors a)) instance (Arg (Neighbors a) a, Arbitrary a) => Arbitrary (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)
+makeFieldsNoPrefix ''Box
+
+-- It seems to be necessary to have an `Arg (V2 a) a` constraint, as a is passed
+-- to V2 internally, in order to make GHC figure out this deriving via correctly.
+deriving via (GenericArbitrary (Box a)) instance (Arg (V2 a) a, Arbitrary a) => Arbitrary (Box a)
+
+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 000000000000..13c4b5d61068
--- /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 000000000000..39953410f2f3
--- /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 000000000000..855a3462daee
--- /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 000000000000..33a98f1ae5a9
--- /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 000000000000..1398c611cf20
--- /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 000000000000..13251d8afdf2
--- /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 000000000000..2b2ee0f96028
--- /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 000000000000..1b875d448302
--- /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 000000000000..2e6d48062a45
--- /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 000000000000..c8153086f1ac
--- /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 000000000000..368b03f25bed
--- /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 000000000000..3ea610795e98
--- /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 000000000000..d13ea8055c2b
--- /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 000000000000..aa6c5fa4fc47
--- /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 000000000000..a0c037a1b4ed
--- /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 000000000000..519a862c6a5a
--- /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 000000000000..b45a91eabed2
--- /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 000000000000..eadd62569663
--- /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 000000000000..14d02872ed4e
--- /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 000000000000..a7021d76cf65
--- /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 000000000000..10f0d831934e
--- /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 000000000000..12c76fc14b2e
--- /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 000000000000..ad3d9cb147da
--- /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 000000000000..cdfcde616d21
--- /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 000000000000..c0501a18a8e0
--- /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 000000000000..fe427c94abf7
--- /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 000000000000..3f4e133fe286
--- /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 000000000000..7f9e1faffedb
--- /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 000000000000..89c23f0de850
--- /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 000000000000..679bfe54597f
--- /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 000000000000..291dfd8b5e46
--- /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 000000000000..5d7b275c8a0b
--- /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 000000000000..c692a3b47944
--- /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 000000000000..154063b5dde2
--- /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 000000000000..2d6c0a280f41
--- /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 000000000000..13b1ba158818
--- /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 000000000000..fc57402e7d8e
--- /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 000000000000..03d534ca39b3
--- /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 000000000000..0be7c0435c5a
--- /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 000000000000..4f8a2f42ee16
--- /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 000000000000..0008eb965c42
--- /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 000000000000..ab7de95e6806
--- /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 000000000000..8abc00b6a2fc
--- /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 000000000000..c273d650821b
--- /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 000000000000..5176880355f4
--- /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 000000000000..db602de56f3a
--- /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 000000000000..b00c803cfe7a
--- /dev/null
+++ b/users/grfn/xanthous/src/Xanthous/Orphans.hs
@@ -0,0 +1,489 @@
+{-# 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           Test.QuickCheck.Arbitrary.Generic (Arg ())
+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 Hashable 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 (Arg (V2 a) a, 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 000000000000..37530cbbc21b
--- /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 000000000000..2cb4299303ba
--- /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 000000000000..329b321b8bda
--- /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 000000000000..f918340f055b
--- /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 000000000000..9e158cc8e2d4
--- /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 000000000000..8e5c04f4bfa9
--- /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 000000000000..0cb009f45ad0
--- /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 000000000000..724f2339dd21
--- /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 000000000000..91d1328e4a10
--- /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 000000000000..dfa65372351d
--- /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 000000000000..aa881b322779
--- /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 000000000000..cffb27cb03f6
--- /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 000000000000..bc08ec1ad24d
--- /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 000000000000..51758d6a25ec
--- /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 000000000000..75c1ebf5e76a
--- /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 000000000000..13f69a808d02
--- /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 000000000000..e403503743c0
--- /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 000000000000..9e8024c9d223
--- /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 000000000000..fd37548ce864
--- /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 000000000000..7c5cad019616
--- /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 000000000000..a7528331627d
--- /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 000000000000..ad81f1984d8f
--- /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 000000000000..acf7a67268f4
--- /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 000000000000..9e67505ba928
--- /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 000000000000..734cce1efbbe
--- /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 000000000000..a6f8401cf75b
--- /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 000000000000..e23f7faba3a6
--- /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 000000000000..b6c80be51be7
--- /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 000000000000..d7a3df4acafa
--- /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 000000000000..34584f73b2ad
--- /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 000000000000..2fa8527d0e59
--- /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 000000000000..b53c657f7559
--- /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 000000000000..2068e338bafe
--- /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 000000000000..2a3873c3b016
--- /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 000000000000..0d800e8a91de
--- /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 000000000000..c88bd9562928
--- /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 000000000000..35ff090b28b9
--- /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 000000000000..61e589280362
--- /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 000000000000..fad841043152
--- /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 000000000000..684a03b2c7a0
--- /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 000000000000..12222c26732f
--- /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.35.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: b3bf8e65d621856081832c9d3c8e8ad38799e23a7f5084dc4f972daa654a0ff3
+
+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 -fconstraint-solver-iterations=6
+  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 -fconstraint-solver-iterations=6 -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 -fconstraint-solver-iterations=6 -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 -fconstraint-solver-iterations=6 -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 000000000000..fa5fd03e0fcd
--- /dev/null
+++ b/users/isomer/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+isomer
diff --git a/users/isomer/keys.nix b/users/isomer/keys.nix
new file mode 100644
index 000000000000..8c29e27895db
--- /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/j4m3s/OWNERS b/users/j4m3s/OWNERS
new file mode 100644
index 000000000000..9d95afbeaaf1
--- /dev/null
+++ b/users/j4m3s/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+j4m3s
diff --git a/users/j4m3s/keys.nix b/users/j4m3s/keys.nix
new file mode 100644
index 000000000000..e5aaa30737d3
--- /dev/null
+++ b/users/j4m3s/keys.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+{
+  all = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH77KDNivaadAX0byGQgmel9hA7VcmFnL+IKYHgBVNVp tvl"
+  ];
+}
diff --git a/users/lukegb/OWNERS b/users/lukegb/OWNERS
new file mode 100644
index 000000000000..4ff54b467e52
--- /dev/null
+++ b/users/lukegb/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+lukegb
diff --git a/users/lukegb/hgext/gerrithook.py b/users/lukegb/hgext/gerrithook.py
new file mode 100644
index 000000000000..ef02126ba0f8
--- /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 000000000000..4745df550c6f
--- /dev/null
+++ b/users/lukegb/keys.nix
@@ -0,0 +1,11 @@
+# My SSH public keys
+{ ... }:
+
+rec {
+  termius = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINytpHct7PLdLNp6MoaOPP7ccBPUQKymVNMqix//Wt1f";
+  porcorosso-nixos = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILid+1rq3k3k7Kbaw8X63vrPrQdanH55TucQwp3ZWfo+";
+  clouvider-lon01-nix = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINQU7Y+Ha5m0ebwUjA55xXT/xbWZAWx1fVNFufle+vQj";
+  lukegb-build = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICESF0H+OCxY/CfyG9VjM6iJe+VbYc4NmGjRrwPCHaD9";
+  lukegb-ca = "cert-authority,principals=\"lukegb\" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEqNOwlR7Qa8cbGpDfSCOweDPbAGQOZIcoRgh6s/J8DR";
+  all = [ termius porcorosso-nixos clouvider-lon01-nix lukegb-build lukegb-ca ];
+}
diff --git a/users/qyliss/OWNERS b/users/qyliss/OWNERS
new file mode 100644
index 000000000000..68724206aff4
--- /dev/null
+++ b/users/qyliss/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+qyliss
diff --git a/users/qyliss/keys.nix b/users/qyliss/keys.nix
new file mode 100644
index 000000000000..d0837a7c6744
--- /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 000000000000..34c668e2fbf5
--- /dev/null
+++ b/users/riking/OWNERS
@@ -0,0 +1 @@
+riking
diff --git a/users/riking/adventofcode-2020/.gitignore b/users/riking/adventofcode-2020/.gitignore
new file mode 100644
index 000000000000..076ff412156a
--- /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 000000000000..a1a18948a7ea
--- /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 000000000000..d90ab548bb7b
--- /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 000000000000..946069e3a68e
--- /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 000000000000..e8bc2a05e45a
--- /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 000000000000..c5ebc34a1f4f
--- /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 000000000000..6f79f9752855
--- /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 000000000000..c2454762bddf
--- /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 000000000000..fa8bff919f40
--- /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 000000000000..8152d31680e7
--- /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 000000000000..109ec353f6db
--- /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 000000000000..876c14c5e31e
--- /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 000000000000..eb48578a52bc
--- /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 000000000000..f04ac830c005
--- /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 000000000000..f47b93511af0
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..b35aedd2dc0f
--- /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 000000000000..9b14613cd4eb
--- /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 000000000000..1f253cb27f0c
--- /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 000000000000..50287098244a
--- /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 000000000000..6434d4ca30f6
--- /dev/null
+++ b/users/sterni/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+sterni
diff --git a/users/sterni/clhs-lookup/README.md b/users/sterni/clhs-lookup/README.md
new file mode 100644
index 000000000000..1f42ff43a210
--- /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 000000000000..0e61dd901f93
--- /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 000000000000..1cde38e8ce3b
--- /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 000000000000..d059b96ce9f0
--- /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 000000000000..b9bc074a8020
--- /dev/null
+++ b/users/sterni/dot-time-man-pages/OWNERS
@@ -0,0 +1 @@
+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 000000000000..c449cde613f9
--- /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 000000000000..cd682fa2eec3
--- /dev/null
+++ b/users/sterni/emacs/default.nix
@@ -0,0 +1,110 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  inherit (pkgs.stdenv.hostPlatform) is64bit;
+
+  # Wrap chktex(1) with the flags we want because the chktex flycheck checker
+  # ignores tex-chktex-extra-flags and has no other way to set flags. I did
+  # not want to mess around with chktexrc because that seems to involve copying
+  # around a lot of rules (that would need to be updated?).
+  #
+  # Warning 8 is about correct dash length. This is really annoying because it'll
+  # light up everywhere if you use typographically correct dashes in German text.
+  chktexLessWarnings = pkgs.writeShellScript "chktex-less-warnings" ''
+    exec chktex -n8 "$@"
+  '';
+
+  # emacsPgtk is defined in emacs-overlay
+  emacs = (pkgs.emacsPackagesFor pkgs.emacsPgtk).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.flycheck
+    epkgs.melpaPackages.haskell-mode
+    epkgs.melpaPackages.hl-todo
+    epkgs.melpaPackages.jq-mode
+    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
+  ] ++ lib.optionals is64bit [
+    epkgs.melpaPackages.languagetool
+  ]);
+
+  configDirectory = pkgs.symlinkJoin {
+    name = "emacs.d";
+    paths = [
+      ./.
+      (pkgs.writeTextFile {
+        name = "injected-emacs.d";
+        destination = "/nix-inject.el";
+        text =
+          # Java doesn't seem to be available for non 64bit platforms in nixpkgs
+          # CBQN doesn't seem to support i686 at least
+          lib.optionalString is64bit ''
+            ;; 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"))
+
+            ;; chktex wrapper that disables warnings I don't want
+            (setq flycheck-tex-chktex-executable "${chktexLessWarnings}")
+            (setq tex-chktex-program "${chktexLessWarnings}")
+
+            (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 000000000000..c38daa109ce5
--- /dev/null
+++ b/users/sterni/emacs/init.el
@@ -0,0 +1,346 @@
+;; set up package infrastructure
+
+(require 'use-package)
+(package-initialize)
+
+;; Set default font and fallback font via set-fontset-font
+(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
+        org-refile-targets '((org-agenda-files . (:maxlevel . 2)))))
+
+;; 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
+(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 flycheck
+  :init (global-flycheck-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-diagnostics nil
+        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 000000000000..ba63da3063fd
--- /dev/null
+++ b/users/sterni/emacs/subscriptions.el
@@ -0,0 +1,71 @@
+;;; 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)
+           ("https://www.stackage.org/feed" dashboard releases)
+           ("http://hundimbuero.blogspot.com/feeds/posts/default?alt=rss" blog cool-and-nice)
+           ("https://text.causal.agency/feed.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://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://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)
+           ("http://blog.nullspace.io/feed.xml" blog)
+           ("https://blog.kingcons.io/rss.xml" blog)
+           ("https://www.imperialviolet.org/iv-rss.xml" blog)
+           ("https://22gato.tumblr.com/rss" pictures cool-and-nice)
+           ("https://theprofoundprogrammer.com/rss" blog)
+           ("http://shitopenlabsays.tumblr.com/rss" openlab)
+           ("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://ccc.de/de/rss/updates.xml" news)
+           ("http://ffaaaffaffaffaa.tumblr.com/rss" pictures)
+           ("http://fotografiona.tumblr.com/rss" pictures)
+           ("http://guteaussicht.org/rss" pictures)
+           ("http://konvergenzfehler.de/feed/" blog)
+           ("https://markuscisler.com/feed.xml" blog)
+           ("http://www.plomlompom.de/PlomRogue/plomwiki.php?action=Blog_Atom" blog)
+           ("http://www.whvrt.de/rss" pictures)
+           ("https://echtsuppe.wordpress.com/feed/" blog defunct)
+           ("https://mgsloan.com/feed.xml" 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 000000000000..b9720d74516d
--- /dev/null
+++ b/users/sterni/exercises/aoc/.gitignore
@@ -0,0 +1,2 @@
+/*/input
+/*/*/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 000000000000..d3ed563ec6f8
--- /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 000000000000..755c9440460a
--- /dev/null
+++ b/users/sterni/exercises/aoc/2021/solutions.bqn
@@ -0,0 +1,484 @@
+#!/usr/bin/env BQN
+
+⟨Xor⟩ ← •Import "../lib.bqn"
+
+#
+# Utilities
+#
+
+⟨IsAsciiNum,ReadInt,ReadDec,SplitOn,_fix⟩ ← •Import •path∾"/../lib.bqn"
+
+ReadInput ← {•file.Lines ∾ •path‿"/input/day"‿(•Fmt 𝕩)}
+
+#
+# 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.>"
+
+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/exercises/aoc/2022/.skip-subtree b/users/sterni/exercises/aoc/2022/.skip-subtree
new file mode 100644
index 000000000000..39d1894495b3
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/.skip-subtree
@@ -0,0 +1 @@
+nix solutions don't use readTree and the rest is non-nix
diff --git a/users/sterni/exercises/aoc/2022/01/1.bqn b/users/sterni/exercises/aoc/2022/01/1.bqn
new file mode 100644
index 000000000000..022b476aa9a9
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/01/1.bqn
@@ -0,0 +1,7 @@
+lib ← •Import •path∾"/../../lib.bqn"
+input ← lib.ReadDec¨¨ (<"") lib.SplitOn •FLines •path∾"/input"
+
+a‿·‿b ← +`3↑∨+´¨ input
+
+•Out "day01.1: "∾•Fmt a
+•Out "day01.2: "∾•Fmt b
diff --git a/users/sterni/exercises/aoc/2022/01/1.k b/users/sterni/exercises/aoc/2022/01/1.k
new file mode 100644
index 000000000000..42d64dfb6cc1
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/01/1.k
@@ -0,0 +1 @@
+(+\e@3#>e:(+/.'1_)'(&0=#'i)_i:0:"input")_1
diff --git a/users/sterni/exercises/aoc/2022/02/2.bqn b/users/sterni/exercises/aoc/2022/02/2.bqn
new file mode 100644
index 000000000000..65e3c817bbd2
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/02/2.bqn
@@ -0,0 +1,7 @@
+lib ← •Import •path∾"/../../lib.bqn"
+i ← 3|"ABCXYZ"⊸⊐¨ ' ' ⊑¨∘lib.SplitOn¨ •FLines •path∾"/input"
+S1 ← {1+𝕩+3×3|1+𝕩-𝕨}
+S2 ← {𝕨 S1 3|𝕨+𝕩-1}
+
+•Out "day02.1: "∾•Fmt +´S1´¨i
+•Out "day02.2: "∾•Fmt +´S2´¨i
diff --git a/users/sterni/exercises/aoc/2022/02/2.k b/users/sterni/exercises/aoc/2022/02/2.k
new file mode 100644
index 000000000000..9b6d10058d77
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/02/2.k
@@ -0,0 +1 @@
++/{{y+1+3*3!1+y-x}[x]'y,3!x+y-1}.'3!"ABCXYZ"?(0:"input")_'1
diff --git a/users/sterni/exercises/aoc/2022/03/3.bqn b/users/sterni/exercises/aoc/2022/03/3.bqn
new file mode 100644
index 000000000000..642fccd45034
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/03/3.bqn
@@ -0,0 +1,8 @@
+i ← •FLines •path∾"/input"
+c ← ∾(↕26)⊸+¨"aA"
+P ← {1+⊑c⊐⊑𝕩}
+S ← (∊/⊣)
+G ← ((⊣/(↕÷˜⟜≠))⊔⊢)
+
+•Out "day03.1: "∾•Fmt +´(P S˝)¨2‿∘⊸⥊¨i
+•Out "day03.2: "∾•Fmt +´3(P S´)¨∘G i
diff --git a/users/sterni/exercises/aoc/2022/03/3.k b/users/sterni/exercises/aoc/2022/03/3.k
new file mode 100644
index 000000000000..3e31f5f32ce2
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/03/3.k
@@ -0,0 +1 @@
++/'(58*r<1)+r:-96+(,/{?y@&~^x?y}/')'((2 0N#)';0N 3#)@\:0:"input"
diff --git a/users/sterni/exercises/aoc/2022/04/4.bqn b/users/sterni/exercises/aoc/2022/04/4.bqn
new file mode 100644
index 000000000000..0b8f1b4500a9
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/04/4.bqn
@@ -0,0 +1,11 @@
+⟨SplitOn, ReadDec⟩ ← •Import "../../lib.bqn"
+
+Sections ← {
+  a‿b ← ReadDec¨ (<'-') SplitOn 𝕩
+  ↕⌾(-⟜a) 1+b
+}
+i ← ∘‿2⥊Sections¨ ∾(<',') SplitOn¨ •FLines "input"
+Is ← ∊´∘((⍋≠¨)⊏⊢)
+
+•Out "day04.1: "∾•Fmt +´(∧´Is)˘ i
+•Out "day04.2: "∾•Fmt +´(∨´Is)˘ i
diff --git a/users/sterni/exercises/aoc/2022/05/5.bqn b/users/sterni/exercises/aoc/2022/05/5.bqn
new file mode 100644
index 000000000000..15b0dfc805b5
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/05/5.bqn
@@ -0,0 +1,18 @@
+⟨ReadDec, SplitOn, IsAsciiNum⟩ ← •Import "../../lib.bqn"
+rs‿rc ← (<"") SplitOn •FLines "../05/input"
+
+stacks ← {
+  count ← '0'-˜⊑⌽' ' (≠/⊢) ⊑⌽rs
+  ' ' (≠/⊢)¨<˘ (count×4) ((»∘(0⊸=)∘(4⊸|)∘↕⊣)/↑) ⍉> (-1)↓rs
+}
+
+cmds ← {0‿1‿1-˜ ReadDec¨ ((∧´IsAsciiNum)¨/⊢) (<' ') SplitOn 𝕩}¨ rc
+
+_ApplyCmd ← {
+  s Fn _self c‿f‿t :
+  m‿k ← 2↑ c ((≤⟜(↕≠))⊔⊢) f⊑s
+  (Fn m)⊸∾⌾(t⊸⊑) k˙⌾(f⊸⊑) s
+}
+
+•Out "day05.1: "∾⊑¨stacks ⌽_ApplyCmd˜´ ⌽ cmds
+•Out "day05.2: "∾⊑¨stacks ⊢_ApplyCmd˜´ ⌽ cmds
diff --git a/users/sterni/exercises/aoc/2022/06/6.bqn b/users/sterni/exercises/aoc/2022/06/6.bqn
new file mode 100644
index 000000000000..041a2e9100d3
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/06/6.bqn
@@ -0,0 +1,4 @@
+i ← ⊑•FLines "input"
+FirstMarker ← {𝕩+⊑/(∧´∘¬⊒)˘𝕩↕i}
+•Out "day06.1: "∾•Fmt FirstMarker 4
+•Out "day06.2: "∾•Fmt FirstMarker 14
diff --git a/users/sterni/exercises/aoc/2022/06/6.k b/users/sterni/exercises/aoc/2022/06/6.k
new file mode 100644
index 000000000000..3dc0de0a3e2d
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/06/6.k
@@ -0,0 +1 @@
+4 14{x+*&x=#'?'x':y}\:1:"input"
diff --git a/users/sterni/exercises/aoc/2022/07/7.bqn b/users/sterni/exercises/aoc/2022/07/7.bqn
new file mode 100644
index 000000000000..2fc387f3406d
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/07/7.bqn
@@ -0,0 +1,24 @@
+lib ← •Import "../../lib.bqn"
+cmds ← 1↓ '$' ((+`= ⟜(⊑¨))⊔⊢) •FLines "input"
+paths ← (<⟨⟩) {
+  𝕨 𝕊 "$ ls": 𝕨;
+  𝕨 𝕊 "$ cd /": ⟨⟩;
+  𝕨 𝕊 "$ cd ..": (-1)↓𝕨;
+  𝕨 𝕊 𝕩: 𝕨∾<5↓𝕩 # "$ cd …"
+}` ⊑¨cmds
+ParseLs ← {
+  dirs‿files ← 2↑((lib.IsAsciiNum∘⊑∘⊑)¨⊔⊢) ((<' ')⊸lib.SplitOn)¨ 1↓𝕩
+  (1⊑¨dirs)⋈(lib.ReadDec 0⊸⊑)¨files
+}
+dirlists ← ParseLs⌾(1⊸⊑)¨⥊⋈˘(("$ cd"⊸≢⟜(4⊸↑)∘⊑¨)∘(1⊸⊏)˘/⊢) (⍒≠¨paths)⊏⍉paths≍cmds
+DirSize ← {⊑𝕨 (⊑∘(1⊸⊑¨∘⊣⊐⊢)⊑⊣) <𝕩}
+DirName ← ∾'/'⊸∾¨
+dirsizes ← ⊑¨ ⟨⟩ {
+  szs 𝕊 ⟨dir, subdirs‿files⟩:
+  Canon ← DirName dir⊸∾⟜⋈
+  sz ← +´files∾szs⊸DirSize∘Canon¨ subdirs
+  szs∾<sz⋈DirName dir
+}˜´ ⌽dirlists
+
+•Out "day07.1: "∾•Fmt +´ 100000 (≥/⊢) dirsizes
+•Out "day07.2: "∾•Fmt (30000000-70000000-⌈´dirsizes) ⌊´∘(≤/⊢) dirsizes
diff --git a/users/sterni/exercises/aoc/2022/08/8.bqn b/users/sterni/exercises/aoc/2022/08/8.bqn
new file mode 100644
index 000000000000..91a16d9573c9
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/08/8.bqn
@@ -0,0 +1,15 @@
+i ← >'0'-˜•FLines "input"
+Visible ← {
+  _vis ← {(⌈`∘(¯1⊸»˘⌾⍉)<⊢)⌾𝕏 𝕗}
+  ∨´𝕩 _vis¨ ⟨⊢,⌽,⍉,⌽⍉⟩
+}
+
+•Out "day08.1: "∾•Fmt +´⥊Visible i
+
+ViewingDistances ← {
+  DirView ← {≠1(»⟜(∧`(⊑𝕩)⊸>)/⊢) 1↓𝕩}
+  _spliceDir ← {! =´≢𝕗 ⋄ 𝕏⁼(⊢↓(⊏⟜(𝕏𝕗))∘⊣)´¨ ⋈⌜˜↕≠𝕗}
+  ×´ DirView¨¨ 𝕩 _spliceDir¨ ⟨⊢, ⌽˘, ⍉, ⌽˘⍉⟩
+}
+
+•Out "day08.2: "∾•Fmt ⌈´⥊ViewingDistances i
diff --git a/users/sterni/exercises/aoc/2022/09/9.bqn b/users/sterni/exercises/aoc/2022/09/9.bqn
new file mode 100644
index 000000000000..fff38b591311
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/09/9.bqn
@@ -0,0 +1,17 @@
+⟨SplitOn,ReadDec⟩ ← •Import "../../lib.bqn"
+i ← ReadDec⌾(1⊸⊑)¨ (<' ')⊸SplitOn¨ •FLines "input"
+
+UnitDelta ← (⊢÷(|+0⊸=))
+ExpandStep ← {
+  𝕊 "L"‿l: 𝕊 (-l)‿0;
+  𝕊 "R"‿r: 𝕊 r‿0;
+  𝕊 "U"‿u: 𝕊 0‿u;
+  𝕊 "D"‿d: 𝕊 0‿(-d);
+  𝕊 delta: ((⌈´|)⥊<∘UnitDelta) delta
+}
+
+Step ← {knots 𝕊 delta: {h 𝕊 t: (UnitDelta h-t) +⍟(1<⌈´|h-t) t}` (delta⊸+)⌾⊑ knots}
+Visited ← {+´0=⊒(¯1⊸⊑)¨(<𝕨⥊<0‿0) Step` ∾ExpandStep¨ 𝕩}
+
+•Out "day09.1: "∾•Fmt  2 Visited i
+•Out "day09.2: "∾•Fmt 10 Visited i
diff --git a/users/sterni/exercises/aoc/2022/10/10.bqn b/users/sterni/exercises/aoc/2022/10/10.bqn
new file mode 100644
index 000000000000..04e3d6a8e563
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/10/10.bqn
@@ -0,0 +1,25 @@
+⟨SplitOn,ReadDec⟩ ← •Import "../../lib.bqn"
+# Instead of implementing the VM described in the problem, translate the
+# program to instructions with equivalent timing for a similar VM that
+# only needs 1 cycle for every instruction.
+is ← ∾{"noop": <"noop"; 𝕩: (<"noop")∾<ReadDec⌾(1⊸⊑) (<' ') SplitOn 𝕩}¨ •FLines "input"
+
+Op ← {x 𝕊 "noop": x;x 𝕊 "addx"‿i: x+i}
+Draw ← {𝕊 c‿x‿pic: pic∨(↕240)((c-1)⊸=∘⊣∧∊)(⌊⌾(÷⟜40)c)+¯1+x+↕3}
+_vm ← {
+  is _self s: (⊑s)≥≠is? s;
+  is _self prev‿sum‿x‿pic:
+  cycle ← prev+1
+  is _self ⟨
+    cycle,
+    sum+x×cycle×⊑cycle∊20‿60‿100‿140‿180‿220,
+    x Op (¯1+cycle)⊑is,
+    Draw cycle‿x‿pic
+  ⟩
+}
+
+·‿sum‿·‿pic ← is _vm 1‿0‿1‿(240⥊0)
+
+•Out "day10.1: "∾•Fmt sum
+•Out "day10.2:"
+•Show ".#" ⊏˜ ∘‿40⥊pic
diff --git a/users/sterni/exercises/aoc/2022/11/11.bqn b/users/sterni/exercises/aoc/2022/11/11.bqn
new file mode 100644
index 000000000000..12b9b5097a59
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/11/11.bqn
@@ -0,0 +1,41 @@
+# needs export BQNLIBS=/path/to/mlochbaum/bqn-libs
+⟨ReadDec,ImportBqnLibs⟩ ← •Import "../../lib.bqn"
+⟨Split⟩ ← ImportBqnLibs "strings.bqn"
+MakeOp ← {
+  𝕊 a‿"+"‿b: 𝕊 a‿+‿b;
+  𝕊 a‿"*"‿b: 𝕊 a‿×‿b;
+  𝕊 a‿op‿b:
+  is‿xs ← (<"old") (≡¨⊔⊢) a‿b
+  {op´ (𝕩⋆≠xs) ∾ReadDec¨ is}
+}
+ParseMonkey ← {
+  ·‿items‿op‿if‿then‿else:
+  {
+    initial ⇐ ReadDec¨ ", " Split 18↓items
+    op ⇐ MakeOp " " Split 19↓op
+    if ⇐ ReadDec 21↓if
+    then ⇐ ReadDec 29↓then
+    else ⇐ ReadDec 30↓else
+  }
+}
+monkeys ← ParseMonkey¨ 1↓' '((+`(≠⟜⊑)¨)⊔⊢)0(≠⟜≠¨/⊢)•FLines "input"
+items ← {𝕩.initial}¨ monkeys
+lim ← ×´{𝕩.if}¨ monkeys
+
+Sim ← {
+  div 𝕊 len:
+  Turn ← {
+    items 𝕊 turnidx:
+    i ← (≠monkeys)|turnidx
+    m ← i⊑monkeys
+
+    worry ← lim|⌊div÷˜ m.Op¨ i⊑items
+    else‿then ← 2↑0 (=⟜(m.if⊸|)⊔⊢) worry
+
+    ⟨then, else⟩⊸(∾˜¨)⌾(m.then‿m.else⊸⊏) ⟨⟩˙⌾(i⊸⊑) items
+  }
+  ×´2↑∨+˝(<items) ((≠⊑)⊸(>((↕⊣)=|)¨)×(≠¨˘)∘>∘(⊣»Turn`)) ↕len×≠items
+}
+
+•Out "day11.1: "∾•Fmt 3 Sim 20
+•Out "day11.2: "∾•Fmt 1 Sim 10000
diff --git a/users/sterni/exercises/aoc/2022/12/12.bqn b/users/sterni/exercises/aoc/2022/12/12.bqn
new file mode 100644
index 000000000000..cf42f6f899ca
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/12/12.bqn
@@ -0,0 +1,16 @@
+⟨ImportBqnLibs,_fix⟩ ← •Import "../../lib.bqn"
+⟨ReplaceAll⟩ ← ImportBqnLibs "strings.bqn"
+i ← >•FLines "input"
+
+elevation ← 'a'-˜⟨"S","E"⟩‿⟨"a","z"⟩ ReplaceAll⌾⥊ i
+starts ← (⊏⟜∞‿0)¨⟨'S'=i,0=elevation⟩
+end ← 'E'=i
+
+Step ← {
+  𝕊 steps:
+  Go ← {𝕏⁼((⊢∾¨↕∘≢)(≤⟜(∞⊸»˘∘+⟜1))˜𝕏elevation)⊑>((⥊⟜∞)∘≢⊸⋈)˜∞⊸»˘1+𝕏steps}
+  steps⌊´Go¨⟨⊢,⌽˘,⍉,⍉⌽⟩
+}
+Shortest ← {⊑end/⊸⊏○⥊Step _fix 𝕩}
+
+•Out¨ "day12.1: "‿"day12.2: "∾¨ •Fmt∘Shortest¨ starts
diff --git a/users/sterni/exercises/aoc/2022/13/13.bqn b/users/sterni/exercises/aoc/2022/13/13.bqn
new file mode 100644
index 000000000000..0242cc5093a1
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/13/13.bqn
@@ -0,0 +1,14 @@
+lib ← •Import "../../lib.bqn"
+str ← lib.ImportBqnLibs "strings.bqn"
+i ← >⟨"[","]"⟩‿⟨"⟨","⟩"⟩⊸(•BQN str.ReplaceAll)¨¨0((⟨⟩⊸≡¨¯1˙⍟⊣¨(+`(=⟜≠)¨))⊔⊢)•FLines "input"
+
+Ord ← {
+  i1 𝕊 i2: 1‿1≡•Type¨ i1‿i2? ¯1‿1‿0⊑˜i1(=+≤)i2;
+  i1 𝕊 l2: 1‿0≡•Type¨ i1‿l2? l2 Ord˜ ⋈i1;
+  l1 𝕊 i2: 0‿1≡•Type¨ l1‿i2? l1 Ord ⋈i2;
+  l1 𝕊 l2: 0‿0≡•Type¨ l1‿l2?
+  ⊑1↑0(≠/⊢)l1 Ord¨○((l1⌈○≠l2)⊸(↑⌾(+⟜1))) l2
+}
+
+•Out "day13.1: "∾•Fmt +´1+/(1⊸=Ord´)˘i
+•Out "day13.2: "∾•Fmt ×´1‿2++´˘¯1=⟨⟨2⟩⟩‿⟨⟨6⟩⟩Ord⌜⥊i
diff --git a/users/sterni/exercises/aoc/2022/15/15.bqn b/users/sterni/exercises/aoc/2022/15/15.bqn
new file mode 100644
index 000000000000..e47355856bf8
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/15/15.bqn
@@ -0,0 +1,18 @@
+lib ← •Import "../../lib.bqn"
+
+F ← ¬∘('-'⊸=∨lib.IsAsciiNum)
+i ← ⌽˘˘∘‿2‿2⥊lib.ReadDec¨>(0⊸<⟜≠¨/⊢)∘((F ¯1˙⍟⊣¨(+`F))⊔⊢)¨ •FLines "input"
+
+ssp ← 4000000
+
+sds ← (⊏˘∾˘(+´˘(|(-˝))˘)) i
+
+# _fix is needed to deal with e.g. ⟨0‿15, 5‿8, 12‿23⟩
+MergeRanges ← ((⊑∾⊑∘⌽)∘∧∘∾)¨∘(+`∘((<∞‿∞)⊸»{<´1‿2⊏𝕨∾𝕩}¨⊢)⊔⊢) lib._fix
+
+Range ← {cky 𝕊 y‿x‿d: x+¯1‿1×d-|cky-y}
+RangesY ← {<˘∧𝕩(⊣Range˘({cky 𝕊 y‿·‿d: d≥|y-cky}˘/⊢))sds}
+OutRangeY ← {(1<≠)◶⟨0˙,𝕩⊸+∘(ssp⊸×⟜(+⟜1))∘(1⊸⊑)∘∾⟩ MergeRanges ssp⌊0⌈RangesY 𝕩}
+
+•Out "day15.1: "∾•Fmt +´-˜´¨MergeRanges RangesY 2÷˜ssp
+•Out "day15.2: "∾•Fmt +´OutRangeY¨↕ssp
diff --git a/users/sterni/exercises/aoc/2022/16/16.k b/users/sterni/exercises/aoc/2022/16/16.k
new file mode 100644
index 000000000000..40d5ace60e84
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/16/16.k
@@ -0,0 +1,21 @@
+/ parsing
+(f;r;t):+{x[1 4],,9_x^'","}'" "\' 0:"input"
+(f;t):`s$''(f;t)
+r:f!{`I$x[&(x<58)&47<x]}' r
+g:f!t
+
+/ total flow scoring
+tf: {+/+\{x,(30-#x)#0}((r.)'x)*`XX=':x}
+
+/ valves to open
+vto: f^(=r).0;
+
+/ paths to keep after each step
+best: {x[(1000&#x)#>tf'x]}
+
+p:{[n;ps]
+  ms:ps[&~fin:(#vto)={#?x[&`X=':x]}'ps];
+  rt: best[ps[&fin],(ms[w],'(,*|)'ms[w:&{(0|/vto=l)&~|/0&':x=l:*|x}'ms]),,/{x,/:,'g[*|x]}' ms];
+  $[n>1;o[n-1;rt];rt]}
+
+*tf'p[29;,,`AA]
diff --git a/users/sterni/exercises/aoc/2022/17/17.bqn b/users/sterni/exercises/aoc/2022/17/17.bqn
new file mode 100644
index 000000000000..21b94221aabd
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/17/17.bqn
@@ -0,0 +1,51 @@
+jets ← '>'= "<>" (∊˜/⊢) •FChars "input"
+pieces ← >¨⟨1‿1‿1‿1⟩‿⟨0‿1‿0,1‿1‿1,0‿1‿0⟩‿⟨0‿0‿1,0‿0‿1,1‿1‿1⟩‿⟨⋈1,⋈1,⋈1,⋈1⟩‿⟨1‿1,1‿1⟩
+w ← 7
+initial ← 0‿w⥊0
+
+# Warning: mutated global!
+ji ← 0
+_try ← {(⊢ 𝕩˙⍟(≠○(+´∘⥊∘∨⟜𝕨)) 𝔽) 𝕩}
+Fall ← {
+  pushed ← 𝕨 ((ji⊑jets)◶«‿»)˘ _try 𝕩
+  ji ↩ (≠jets)|ji+1
+  fallen ← 𝕨 » _try pushed
+  𝕨 𝕊⍟(pushed≢fallen) fallen
+}
+Height ← ≠∘(∨´˘/⊢)
+ThrowPiece ← {
+  piece ← 𝕩 (|˜⟜≠⊑⊢) pieces
+  chamber ← (((3+≠piece)⊸+∘⊑∘(1⊸↑)∘⌽∘(1⊸+)∘/∨´˘)↑⊢)⌾⌽𝕨
+  falling ← (≠chamber)↑(»⍟2 w⊸↑)˘piece
+  chamber (⊣∨Fall) falling
+}
+
+•Out "day17.1: "∾•Fmt Height initial ThrowPiece˜´ ⌽↕2022
+
+# https://mlochbaum.github.io/BQN/doc/control.html#while
+While ← {𝕩{𝔽⍟𝔾∘𝔽_𝕣_𝔾∘𝔽⍟𝔾𝕩}𝕨@}´
+{
+  target ← 1000000000000
+  ji ↩ 0 ⋄ i ← 0 ⋄ res ← @
+
+  chamber ← initial
+  cycles ← ⟨≠pieces,≠jets⟩⥊<⟨⟩
+
+  While {𝕤⋄res=@}‿{𝕤
+    chamber ↩ chamber ThrowPiece i
+    i +↩ 1
+
+    t ← i|˜≠pieces
+    cycles ↩ {
+      new ← 𝕩∾<i⋈Height chamber
+      res ↩ {𝕊 𝕩:
+        ⟨pl,hl⟩‿· ← chk ← ¯2↑new
+        pd‿hd ← -´⌽chk
+        @˙⍟(0≠pd|target-pl) hl+hd×pd÷˜target-pl
+      }⍟(1<≠new) @
+      new
+    }⌾(t‿ji⊸⊑) cycles
+  }
+
+  •Out "day17.2: "∾•Fmt res
+}
diff --git a/users/sterni/exercises/aoc/2022/18/18.bqn b/users/sterni/exercises/aoc/2022/18/18.bqn
new file mode 100644
index 000000000000..76ec569fed41
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/18/18.bqn
@@ -0,0 +1,14 @@
+lib ← •Import "../../lib.bqn"
+
+i ← (lib.ReadDec¨(<',')⊸lib.SplitOn)¨ •FLines "input"
+dim ← 1+⌈´i
+cubes ← i∊˜↕dim
+
+views ← ⟨0‿1‿2, 1‿2‿0, 2‿0‿1⟩
+Exposed ← {(6×+´⥊𝕩)-2×+´views{+´⥊(∧˝˘)2↕𝕨⍉𝕩}¨<𝕩}
+Interior ← {(¬𝕩)∧´views{((lib.Xor`∘((∊∧⊢)∨»∘(∊⌾⌽∧⊢)))⎉1)⌾(𝕨⊸⍉)𝕩}¨<𝕩}
+Displace ← {⌈´(⥊⊢‿⌽⋈⌜views){F‿a 𝕊 𝕩:((-∘¬∘(»((0⊸=⊣)∧>)⊢)⌈⊢)⎉1)⌾(F a⊸⍉)𝕩}¨<𝕩}
+Exterior ← (⊢-○Exposed ¯1⊸=∘(Displace lib._fix)∘(-∘Interior+⊢))
+
+•Out "day18.1: "∾•Fmt Exposed cubes
+•Out "day18.2: "∾•Fmt Exterior cubes
diff --git a/users/sterni/exercises/aoc/2022/20/20.bqn b/users/sterni/exercises/aoc/2022/20/20.bqn
new file mode 100644
index 000000000000..8d4c905e8749
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/20/20.bqn
@@ -0,0 +1,13 @@
+⟨ReadDec⟩ ← •Import "../../lib.bqn"
+enc ← ReadDec¨ •FLines "input"
+
+CoordSum ← +´∘(1000‿2000‿3000⊸((⊢≠⊸|+⟜(⊑∘(/=⟜0)∘⊢))⊏⊢))
+Mix ← {
+  M ← {m 𝕊 i:
+    l ← ≠m
+    i {n ← (l-1)|(𝕩⊑m)+⊑/𝕩=𝕨 ⋄ (n⊸↑(∾⟜𝕩)⊸∾n⊸↓) 𝕩(≠/⊢)𝕨}˜´ ⌽↕l
+  }
+  CoordSum ((⊢M⍟𝕨↕∘≠)⊏⊢) 𝕩
+}
+•Out "day20.1: "∾•Fmt 1 Mix enc
+•Out "day20.2: "∾•Fmt 10 Mix 811589153×enc
diff --git a/users/sterni/exercises/aoc/2022/21/21.bqn b/users/sterni/exercises/aoc/2022/21/21.bqn
new file mode 100644
index 000000000000..2f91f55d445e
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/21/21.bqn
@@ -0,0 +1,25 @@
+⟨ImportBqnLibs, IsAsciiNum, ReadDec⟩ ← •Import "../../lib.bqn"
+⟨ReplaceAll, Split⟩ ← ImportBqnLibs "strings.bqn"
+
+i ← ": "⊸Split¨ •FLines "input"
+ReplaceInts ← {
+  𝕊 𝕩: 𝕊´ 2↑(¬∘(∧´IsAsciiNum∘⊑∘⌽)¨⊔⊢) 𝕩;
+  # TODO: Efficient replace on tokens
+  is 𝕊 es: (((•Fmt⍟(0⊸≠•Type))¨⌾(1⊸⊑) <˘⍉>is)⊸ReplaceAll⌾(1⊸⊑))¨ es
+}
+
+c ← 0
+CanEval ← (IsAsciiNum∨∊⟜"+-/* ")
+Eval ← {
+  a‿s‿b ← " " Split 𝕩
+  f ← ⊑+‿-‿×‿÷⊏˜"+-*/"⊐s
+  a F○ReadDec b
+}
+EvalExprs ← {
+  p‿e ← 2↑((∧´CanEval∘⊑∘⌽)¨⊔⊢) 𝕩
+  ev ← (Eval⌾(⊑⌽))¨ e
+  c +↩1
+  (⊑(⊑¨ev)∊˜<"root")◶⟨EvalExprs∘(ReplaceInts⟜p),1⊸⊑⊑⟩ ev
+}
+
+•Show EvalExprs ReplaceInts i
diff --git a/users/sterni/exercises/aoc/2022/25/25.bqn b/users/sterni/exercises/aoc/2022/25/25.bqn
new file mode 100644
index 000000000000..921099141f09
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/25/25.bqn
@@ -0,0 +1,4 @@
+c ← "=-012"
+F ← +´∘(⊢×(5⊸⋆)∘⌽∘↕∘≠)∘(-⟜2)∘(c⊸⊐)
+T ← {c⊏˜5|2+𝕩 {(⌊5÷˜𝕨+2) (𝕊⟜(𝕨⊸∾))⍟(0<𝕨) 𝕩} ⟨⟩}
+•Out "day25.1: "∾T +´F¨ •FLines "input"
diff --git a/users/sterni/exercises/aoc/2022/25/25.k b/users/sterni/exercises/aoc/2022/25/25.k
new file mode 100644
index 000000000000..df956f002f2e
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/25/25.k
@@ -0,0 +1 @@
+c@2+{(1_o,0)+x+-5*o:2<x}/5\+/{5/x-2}'(c:"=-012")?0:"input"
diff --git a/users/sterni/exercises/aoc/2022/README.md b/users/sterni/exercises/aoc/2022/README.md
new file mode 100644
index 000000000000..65d51dd21fe7
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/README.md
@@ -0,0 +1,8 @@
+# sterni's [Advent of Code 2022](adventofcode.com/2022)
+
+I'm trying to do it in BQN again to redeem myself for my unfinished [AoC 2021](../2021),
+but will allow myself falling back to another language if I get stuck, so I actually
+complete this one.
+~~I also plan to write additional solutions in Nix (when I have the time) in order to
+throw `//tvix/eval` against some new problems.~~
+We'll see how it goes, as my December promises to be quite busy.
diff --git a/users/sterni/exercises/aoc/2022/default.nix b/users/sterni/exercises/aoc/2022/default.nix
new file mode 100644
index 000000000000..01134d130697
--- /dev/null
+++ b/users/sterni/exercises/aoc/2022/default.nix
@@ -0,0 +1,53 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  inherit (pkgs.buildPackages) cbqn ngn-k;
+
+  # input files are not checked in
+  meta.ci.skip = true;
+
+  BQNLIBS = pkgs.fetchFromGitHub {
+    owner = "mlochbaum";
+    repo = "bqn-libs";
+    rev = "d56d8ea0b8c294fac7274678d9ab112553a03f42";
+    sha256 = "1c1bkqj62v8m13jgaa32ridy0fk5iqysq5b2qwxbqxhky5zwnk9h";
+  };
+in
+
+depot.nix.readTree.drvTargets {
+  shell = pkgs.mkShell {
+    name = "aoc-2022-shell";
+    packages = [
+      cbqn
+      ngn-k
+    ];
+
+    inherit BQNLIBS;
+  };
+
+  bqn = pkgs.runCommand "bqn-aoc-2022"
+    {
+      nativeBuildInputs = [
+        cbqn
+      ];
+
+      aoc = builtins.path {
+        name = "bqn-aoc-2022";
+        path = ./../.;
+        # Need lib.bqn from ../ and all inputs as well as bqn files from ./*
+        filter = path: type:
+          lib.hasSuffix ".bqn" path || (
+            lib.hasPrefix (toString ./.) path
+            && (
+              type == "directory"
+              || lib.hasSuffix "/input" path
+            )
+          );
+      };
+
+      inherit meta BQNLIBS;
+    }
+    ''
+      find "$aoc/2022" -name '*.bqn' -exec BQN {} \; | tee "$out"
+    '';
+}
diff --git a/users/sterni/exercises/aoc/lib.bqn b/users/sterni/exercises/aoc/lib.bqn
new file mode 100644
index 000000000000..e870a5dfa426
--- /dev/null
+++ b/users/sterni/exercises/aoc/lib.bqn
@@ -0,0 +1,18 @@
+IsAsciiNum ⇐ ('0'⊸≤∧≤⟜'9')
+IsAlpha ⇐ (('a'⊸≤∧≤⟜'z')∨('A'⊸≤∧≤⟜'Z'))
+
+# based on leah2's function
+ReadInt ⇐ {
+  𝕨 𝕊 𝕩: '-'=⊑𝕩? -𝕨 𝕊 1↓𝕩;
+  𝕨 𝕊 𝕩: (𝕨⊸×+⊣)´∘⌽-⟜'0'𝕩
+}
+ReadDec ⇐ 10⊸ReadInt
+
+SplitOn ⇐ ((⊢ (-1˙)⍟⊣¨ +`∘(1⊸»<⊢))∘(≡¨)⊔⊢)
+SplitAt ← ((⊣≤↕∘≠∘⊢)⊔⊢)
+
+_fix ⇐ {𝕩 𝕊∘⊢⍟≢ 𝔽 𝕩}
+
+ImportBqnLibs ⇐ {•Import 𝕩∾˜"/"∾˜¯1↓1⊑•SH "printenv"‿"BQNLIBS"}
+
+Xor ⇐ (¬⊸∧∨∧⟜¬)
diff --git a/users/sterni/external/flipdot-gschichtler.nix b/users/sterni/external/flipdot-gschichtler.nix
new file mode 100644
index 000000000000..58f4fe1e7c8f
--- /dev/null
+++ b/users/sterni/external/flipdot-gschichtler.nix
@@ -0,0 +1,9 @@
+{ pkgs, depot, ... }:
+
+import depot.users.sterni.external.sources.flipdot-gschichtler { inherit pkgs; } // {
+  # all targets we care about for depot
+  meta.ci.targets = [
+    "bahnhofshalle"
+    "warteraum"
+  ];
+}
diff --git a/users/sterni/external/likely-music.nix b/users/sterni/external/likely-music.nix
new file mode 100644
index 000000000000..cfb6d120bdd9
--- /dev/null
+++ b/users/sterni/external/likely-music.nix
@@ -0,0 +1,11 @@
+{ depot, pkgs, ... }:
+
+import depot.users.sterni.external.sources.likely-music
+  {
+    inherit pkgs;
+    inherit (depot.third_party) napalm;
+  } // {
+  meta.ci.targets = [
+    "likely-music"
+  ];
+}
diff --git a/users/sterni/external/sources.json b/users/sterni/external/sources.json
new file mode 100644
index 000000000000..5233aed23e67
--- /dev/null
+++ b/users/sterni/external/sources.json
@@ -0,0 +1,26 @@
+{
+    "flipdot-gschichtler": {
+        "branch": "master",
+        "description": "send text to the flipdots, orderly queued",
+        "homepage": "https://flipdot.openlab-augsburg.de",
+        "owner": "openlab-aux",
+        "repo": "flipdot-gschichtler",
+        "rev": "93683a7fff04e167963b70a8906f982567646501",
+        "sha256": "134kgmlv63vzdvc3lr0rys55klmzip7qpfnyzssahihp4mjyyq16",
+        "type": "tarball",
+        "url": "https://github.com/openlab-aux/flipdot-gschichtler/archive/93683a7fff04e167963b70a8906f982567646501.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "likely-music": {
+        "branch": "master",
+        "description": "experimental application for probabilistic music composition",
+        "homepage": "",
+        "owner": "sternenseemann",
+        "repo": "likely-music",
+        "rev": "c9bef141d846c493a045385ab8146aa28fc8ef33",
+        "sha256": "1wqgxx8wk7lrvyn9h66gga2wf7dcq7si8wq1w5gfhjnwnsrnvs6y",
+        "type": "tarball",
+        "url": "https://github.com/sternenseemann/likely-music/archive/c9bef141d846c493a045385ab8146aa28fc8ef33.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    }
+}
diff --git a/users/sterni/external/sources.nix b/users/sterni/external/sources.nix
new file mode 100644
index 000000000000..cdcde6da5c54
--- /dev/null
+++ b/users/sterni/external/sources.nix
@@ -0,0 +1,197 @@
+# This file has been generated by Niv.
+_:
+let
+
+  #
+  # The fetchers. fetch_<type> fetches specs of type <type>.
+  #
+
+  fetch_file = pkgs: name: spec:
+    let
+      name' = sanitizeName name + "-src";
+    in
+    if spec.builtin or true then
+      builtins_fetchurl { inherit (spec) url sha256; name = name'; }
+    else
+      pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
+
+  fetch_tarball = pkgs: name: spec:
+    let
+      name' = sanitizeName 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 = name: spec:
+    let
+      ref =
+        if spec ? ref then spec.ref else
+        if spec ? branch then "refs/heads/${spec.branch}" else
+        if spec ? tag then "refs/tags/${spec.tag}" else
+        abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
+      submodules = if spec ? submodules then spec.submodules else false;
+      submoduleArg =
+        let
+          nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0;
+          emptyArgWithWarning =
+            if submodules == true
+            then
+              builtins.trace
+                (
+                  "The niv input \"${name}\" uses submodules "
+                  + "but your nix's (${builtins.nixVersion}) builtins.fetchGit "
+                  + "does not support them"
+                )
+                { }
+            else { };
+        in
+        if nixSupportsSubmodules
+        then { inherit submodules; }
+        else emptyArgWithWarning;
+    in
+    builtins.fetchGit
+      ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg);
+
+  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
+  #
+
+  # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
+  sanitizeName = name:
+    (
+      concatMapStrings (s: if builtins.isList s then "-" else s)
+        (
+          builtins.split "[^[:alnum:]+._?=-]+"
+            ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
+        )
+    );
+
+  # The set of packages used when specs are fetched using non-builtins.
+  mkPkgs = sources: system:
+    let
+      sourcesNixpkgs =
+        import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
+      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 name spec
+    else if spec.type == "tarball" then fetch_tarball pkgs name spec
+    else if spec.type == "git" then fetch_git name 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
+      # this turns the string into an actual Nix path (for both absolute and
+      # relative paths)
+    if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${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));
+  concatMapStrings = f: list: concatStrings (map f list);
+  concatStrings = builtins.concatStringsSep "";
+
+  # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
+  optionalAttrs = cond: as: if cond then as else { };
+
+  # fetchTarball version that is compatible between all the versions of Nix
+  builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
+    let
+      inherit (builtins) lessThan nixVersion fetchTarball;
+    in
+    if lessThan nixVersion "1.12" then
+      fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
+    else
+      fetchTarball attrs;
+
+  # fetchurl version that is compatible between all the versions of Nix
+  builtins_fetchurl = { url, name ? null, sha256 }@attrs:
+    let
+      inherit (builtins) lessThan nixVersion fetchurl;
+    in
+    if lessThan nixVersion "1.12" then
+      fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
+    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)
+    , system ? builtins.currentSystem
+    , pkgs ? mkPkgs sources system
+    }: 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/users/sterni/htmlman/README.md b/users/sterni/htmlman/README.md
new file mode 100644
index 000000000000..258233d4c4d2
--- /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 000000000000..6bf21ce2dbfd
--- /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 000000000000..a44b5ef06934
--- /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 000000000000..0a422bc0d136
--- /dev/null
+++ b/users/sterni/keys.nix
@@ -0,0 +1,8 @@
+{ ... }:
+
+{
+  all = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJk+KvgvI2oJTppMASNUfMcMkA2G5ZNt+HnWDzaXKLlo lukas@wolfgang"
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIZTrefeqOlXDz7nnDWw820+29vLgn6R3o4N1G3lRWrr lukas@ludwig"
+  ];
+}
diff --git a/users/sterni/lv/gopher/default.nix b/users/sterni/lv/gopher/default.nix
new file mode 100644
index 000000000000..f8f42c82d56f
--- /dev/null
+++ b/users/sterni/lv/gopher/default.nix
@@ -0,0 +1,8 @@
+{ depot, ... }:
+
+depot.users.sterni.nix.build.buildGopherHole {
+  name = "gopher-sterni.lv";
+  dir = [
+    "🚧 closed for construction 🚧"
+  ];
+}
diff --git a/users/sterni/machines/.skip-subtree b/users/sterni/machines/.skip-subtree
new file mode 100644
index 000000000000..a79762853edd
--- /dev/null
+++ b/users/sterni/machines/.skip-subtree
@@ -0,0 +1 @@
+Subdirectories are manually reexposed by default.nix as the contain NixOS modules
diff --git a/users/sterni/machines/default.nix b/users/sterni/machines/default.nix
new file mode 100644
index 000000000000..4a859a337a22
--- /dev/null
+++ b/users/sterni/machines/default.nix
@@ -0,0 +1,77 @@
+{ depot, lib, pkgs, ... }:
+
+let
+  bins = depot.nix.getBins pkgs.nq [ "fq" "nq" ];
+
+  machines = lib.mapAttrs
+    (name: _:
+      depot.ops.nixos.nixosFor (import (./. + ("/" + name)))
+    )
+    (lib.filterAttrs (_: type: type == "directory") (builtins.readDir ./.));
+
+  # TODO(sterni): share code with rebuild-system
+  localDeployScriptFor = { system, ... }:
+    pkgs.writeShellScript "local-deploy-${system.name}" ''
+      set -eu
+      nix-env -p /nix/var/nix/profiles/system --set "${system}"
+      "${system}/bin/switch-to-configuration" switch
+    '';
+
+  # Builds the system on the remote machine
+  deployScriptFor = { system, ... }@machine:
+    pkgs.writeShellScript "remote-deploy-${system.name}" ''
+      set -eu
+
+      if [ $# != 1 ]; then
+        printf 'usage: %s [USER@]HOST' "$0"
+        exit 100
+      fi
+
+      readonly TARGET_HOST="$1"
+      readonly DEPLOY_DRV="${
+        builtins.unsafeDiscardOutputDependency (
+          # Wrapper script around localDeployScriptFor that merely starts the
+          # local deploy script using and nq and then waits using fq. This means
+          # we can't Ctrl-C the deploy and it won't be terminated by a lost
+          # connection.
+          pkgs.writeShellScript "queue-deploy-${system.name}" ''
+            readonly STATE_DIR="''${XDG_STATE_HOME:-$HOME/.local/state}/sterni-deploy"
+            mkdir -p "$STATE_DIR"
+
+            export NQDIR="$STATE_DIR"
+
+            "${bins.nq}" "${localDeployScriptFor machine}"
+            "${bins.fq}"
+          ''
+        ).drvPath
+      }"
+
+      nix-copy-closure -s --gzip --to "$TARGET_HOST" "$DEPLOY_DRV"
+
+      readonly DEPLOY_OUT="$(ssh "$TARGET_HOST" "nix-store -r '$DEPLOY_DRV'")"
+
+      ssh "$TARGET_HOST" "$DEPLOY_OUT"
+    '';
+
+in
+
+depot.nix.readTree.drvTargets (
+  # this somehow becomes necessarily ugly with nixpkgs-fmt
+  machines // { inherit deployScriptFor; } //
+
+  lib.mapAttrs'
+    (name: _: {
+      name = "${name}System";
+      value = machines.${name}.system;
+    })
+    machines
+
+    //
+
+  lib.mapAttrs'
+    (name: _: {
+      name = "${name}Deploy";
+      value = deployScriptFor machines.${name};
+    })
+    machines
+)
diff --git a/users/sterni/machines/edwin/default.nix b/users/sterni/machines/edwin/default.nix
new file mode 100644
index 000000000000..de34aa78afe4
--- /dev/null
+++ b/users/sterni/machines/edwin/default.nix
@@ -0,0 +1,88 @@
+{ config, lib, pkgs, depot, ... }:
+
+{
+  imports = [
+    # Third party modules we use
+    "${depot.third_party.agenix.src}/modules/age.nix"
+    # These modules touch things related to booting (filesystems, initrd network…)
+    ./hardware.nix
+    ./network.nix
+    # These modules configure services, websites etc.
+    ../../modules/disk-checkup.nix
+    ./minecraft.nix
+    ./gopher.nix
+    ./http/sterni.lv.nix
+    ./http/code.sterni.lv.nix
+    ./http/flipdot.openlab-augsburg.de.nix
+    ./http/likely-music.sterni.lv.nix
+  ];
+
+  config = {
+    time.timeZone = "Europe/Berlin";
+
+    nixpkgs.config.allowUnfreeRedistributable = true;
+    nix = {
+      package = pkgs.nix_2_3;
+      settings = {
+        trusted-public-keys = lib.mkAfter [
+          "headcounter.org:/7YANMvnQnyvcVB6rgFTdb8p5LG1OTXaO+21CaOSBzg="
+        ];
+        substituters = lib.mkAfter [
+          "https://hydra.build"
+        ];
+      };
+    };
+    tvl.cache.enable = true;
+
+    services = {
+      journald.extraConfig = ''
+        SystemMaxUse=1024M
+      '';
+
+      openssh.enable = true;
+    };
+
+    security.acme = {
+      defaults.email = builtins.getAttr "email" (
+        builtins.head (
+          builtins.filter (attrs: attrs.username == "sterni") depot.ops.users
+        )
+      );
+      acceptTerms = true;
+    };
+
+    programs = {
+      fish.enable = true;
+      mosh.enable = true;
+      tmux.enable = true;
+    };
+
+    environment.systemPackages = [
+      pkgs.weechat
+      pkgs.wget
+      pkgs.git
+      pkgs.stow
+      pkgs.htop
+      pkgs.foot.terminfo
+      pkgs.vim
+    ];
+
+    users = {
+      users = {
+        root.openssh.authorizedKeys.keys = depot.users.sterni.keys.all;
+        lukas = {
+          isNormalUser = true;
+          extraGroups = [ "wheel" "http" "git" ];
+          openssh.authorizedKeys.keys = depot.users.sterni.keys.all;
+          shell = "${pkgs.fish}/bin/fish";
+        };
+      };
+    };
+
+    nix.settings.trusted-users = [
+      "lukas"
+    ];
+
+    system.stateVersion = "20.09";
+  };
+}
diff --git a/users/sterni/machines/edwin/gopher.nix b/users/sterni/machines/edwin/gopher.nix
new file mode 100644
index 000000000000..57275e13a55a
--- /dev/null
+++ b/users/sterni/machines/edwin/gopher.nix
@@ -0,0 +1,19 @@
+{ depot, ... }:
+
+{
+  config = {
+    services.spacecookie = {
+      enable = true;
+      openFirewall = true;
+      settings = {
+        hostname = "sterni.lv";
+        root = depot.users.sterni.lv.gopher;
+        log = {
+          enable = true;
+          hide-ips = true;
+          hide-time = true;
+        };
+      };
+    };
+  };
+}
diff --git a/users/sterni/machines/edwin/hardware.nix b/users/sterni/machines/edwin/hardware.nix
new file mode 100644
index 000000000000..258410894a80
--- /dev/null
+++ b/users/sterni/machines/edwin/hardware.nix
@@ -0,0 +1,64 @@
+{ config, lib, pkgs, depot, ... }:
+
+{
+  config = {
+    boot = {
+      loader.grub = {
+        enable = true;
+        version = 2;
+        # TODO(sterni): use /dev/disk/by-id ?
+        devices = [
+          "/dev/sda"
+          "/dev/sdb"
+        ];
+      };
+
+      kernelModules = [
+        "kvm-intel"
+      ];
+
+      initrd.availableKernelModules = [
+        "ahci"
+        "sd_mod"
+        "btrfs"
+        "realtek"
+        "r8169"
+      ];
+    };
+
+    boot.initrd.luks.devices = {
+      "crypt1".device = "/dev/disk/by-uuid/02ac34ee-be10-401b-90c2-1c6aa54c4d5f";
+      "crypt2".device = "/dev/disk/by-uuid/7ce07191-e704-4aed-a60f-dfa3ce386b26";
+      "crypt-swap1".device = "/dev/disk/by-uuid/fec7155c-6a65-4f25-b271-43763e4c31eb";
+      "crypt-swap2".device = "/dev/disk/by-uuid/7b0a03fc-51de-4578-9811-94b00df09d88";
+    };
+
+    fileSystems = {
+      "/" = {
+        device = "/dev/disk/by-label/root";
+        fsType = "btrfs";
+      };
+
+      "/boot" = {
+        device = "/dev/disk/by-label/boot";
+        fsType = "btrfs";
+      };
+    };
+
+    swapDevices = [
+      { device = "/dev/disk/by-label/swap1"; }
+      { device = "/dev/disk/by-label/swap2"; }
+    ];
+
+    powerManagement.cpuFreqGovernor = "performance";
+    hardware = {
+      enableRedistributableFirmware = true;
+      cpu.intel.updateMicrocode = true;
+    };
+
+    nix.settings = {
+      max-jobs = 2;
+      cores = 4;
+    };
+  };
+}
diff --git a/users/sterni/machines/edwin/http/code.sterni.lv.nix b/users/sterni/machines/edwin/http/code.sterni.lv.nix
new file mode 100644
index 000000000000..7fd730285914
--- /dev/null
+++ b/users/sterni/machines/edwin/http/code.sterni.lv.nix
@@ -0,0 +1,262 @@
+{ depot, pkgs, lib, config, ... }:
+
+let
+  virtualHost = "code.sterni.lv";
+
+  repoSections = [
+    {
+      section = "active";
+      repos = {
+        spacecookie = {
+          description = "gopher server (and library for Haskell)";
+          upstream = "https://github.com/sternenseemann/spacecookie.git";
+        };
+        "mirror/depot" = {
+          description = "monorepo for the virus lounge";
+          upstream = "https://code.tvl.fyi/depot.git";
+          cgit.defbranch = "canon";
+        };
+        "mirror/flipdot-gschichtler" = {
+          description = "message queue system for OpenLab's flipdot display";
+          upstream = "https://github.com/openlab-aux/flipdot-gschichtler.git";
+        };
+        "mirror/nixpkgs" = {
+          description = "Nix packages collection";
+          upstream = "https://github.com/nixos/nixpkgs.git";
+          cgit.enable-commit-graph = "0"; # too slow
+        };
+        "mirror/vuizvui" = {
+          description = "Nix(OS) expressions used by the OpenLab and its members";
+          upstream = "https://github.com/openlab-aux/vuizvui.git";
+        };
+      };
+    }
+    {
+      section = "poc";
+      repos = {
+        emoji-generic = {
+          description = "generic emoji library for Haskell";
+          upstream = "https://github.com/sternenseemann/emoji-generic.git";
+        };
+        grav2ty = {
+          description = "“realistic” 2d space game";
+          upstream = "https://github.com/sternenseemann/grav2ty.git";
+        };
+        haskell-dot-time = {
+          description = "UTC-centric time library for haskell with dot time support";
+          cgit.defbranch = "main";
+        };
+        buchstabensuppe = {
+          description = "toy font rendering for low pixelcount, high contrast displays";
+          upstream = "https://github.com/sternenseemann/buchstabensuppe.git";
+          cgit.defbranch = "main";
+        };
+        "mirror/saneterm" = {
+          description = "modern line-oriented terminal emulator without support for TUIs";
+          upstream = "https://git.8pit.net/saneterm.git";
+        };
+      };
+    }
+    {
+      # TODO(sterni): resisort, klammeraffe, cl-ca, ponify, tinyrl
+      section = "archive";
+      repos = {
+        gopher-proxy = {
+          description = "Gopher over HTTP proxy";
+          upstream = "https://github.com/sternenseemann/gopher-proxy.git";
+        };
+        likely-music = {
+          description = "experimental application for probabilistic music composition";
+          upstream = "https://github.com/sternenseemann/likely-music.git";
+        };
+        logbook = {
+          description = "file format for keeping a personal log";
+          upstream = "https://github.com/sternenseemann/logbook.git";
+        };
+        sternenblog = {
+          description = "file based cgi blog software";
+          upstream = "https://github.com/sternenseemann/sternenblog.git";
+        };
+      };
+    }
+  ];
+
+  repoPath = name: repo: repo.path or "/srv/git/${name}.git";
+
+  cgitRepoEntry = name: repo:
+    lib.concatStringsSep "\n" (
+      [
+        "repo.url=${name}"
+        "repo.path=${repoPath name repo}"
+      ]
+      ++ lib.optional (repo ? description) "repo.desc=${repo.description}"
+      ++ lib.mapAttrsToList (n: v: "repo.${n}=${v}") repo.cgit or { }
+    );
+
+  cgitHead = pkgs.writeText "cgit-head.html" ''
+    <style>
+    #summary {
+      max-width: 80em;
+    }
+
+    #summary * {
+      max-width: 100%;
+    }
+    </style>
+  '';
+
+  cgitConfig = pkgs.writeText "cgitrc" ''
+    virtual-root=/
+
+    enable-http-clone=1
+    clone-url=https://${virtualHost}/$CGIT_REPO_URL
+
+    enable-blame=1
+    enable-log-filecount=1
+    enable-log-linecount=1
+    enable-index-owner=0
+    enable-blame=1
+    enable-commit-graph=1
+
+    root-title=code.sterni.lv
+    css=/cgit.css
+    head-include=${cgitHead}
+
+    mimetype-file=${pkgs.mime-types}/etc/mime.types
+
+    about-filter=${depot.tools.cheddar.about-filter}/bin/cheddar-about
+    source-filter=${depot.tools.cheddar}/bin/cheddar
+    readme=:README.md
+    readme=:readme.md
+
+    section-sort=0
+    ${
+      lib.concatMapStringsSep "\n" (section:
+        ''
+          section=${section.section}
+
+        ''
+        + builtins.concatStringsSep "\n\n" (lib.mapAttrsToList cgitRepoEntry section.repos)
+      ) repoSections
+    }
+  '';
+
+  /* Merge a list of attrs, but fail when the same attribute occurs twice.
+
+     Type: [ attrs ] -> attrs
+  */
+  mergeManyDistinctAttrs = lib.foldAttrs
+    (
+      val: nul:
+        if nul == null then val else throw "Every attribute name may occur only once"
+    )
+    null;
+
+  flatRepos = mergeManyDistinctAttrs
+    (builtins.map (section: section.repos) repoSections);
+
+  reposToMirror = lib.filterAttrs (_: repo: repo ? upstream) flatRepos;
+
+  # User and group name used for running the mirror scripts
+  mirroredReposOwner = "git";
+
+  # Make repo name suitable for systemd unit/timer
+  unitName = name: "mirror-${lib.strings.sanitizeDerivationName name}";
+in
+
+{
+  imports = [
+    ./nginx.nix
+    ./fcgiwrap.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."${virtualHost}" = {
+      enableACME = true;
+      forceSSL = true;
+      root = "${pkgs.cgit-pink}/cgit/";
+      extraConfig = ''
+        try_files $uri @cgit;
+
+        location @cgit {
+          include ${pkgs.nginx}/conf/fastcgi_params;
+          fastcgi_param    SCRIPT_FILENAME ${pkgs.cgit-pink}/cgit/cgit.cgi;
+          fastcgi_param    PATH_INFO       $uri;
+          fastcgi_param    QUERY_STRING    $args;
+          fastcgi_param    HTTP_HOST       $server_name;
+          fastcgi_param    CGIT_CONFIG     ${cgitConfig};
+          fastcgi_pass     unix:${toString config.services.fcgiwrap.socketAddress};
+        }
+      '';
+    };
+
+    users = {
+      users.${mirroredReposOwner} = {
+        group = mirroredReposOwner;
+        isSystemUser = true;
+      };
+
+      groups.${mirroredReposOwner} = { };
+    };
+
+
+    systemd.timers = lib.mapAttrs'
+      (
+        name: repo:
+          {
+            name = unitName name;
+            value = {
+              description = "regularly update mirror git repository ${name}";
+              wantedBy = [ "timers.target" ];
+              enable = true;
+              timerConfig = {
+                # Fire every 6h and distribute the workload over next 6h randomly
+                OnCalendar = "*-*-* 00/6:00:00";
+                AccuracySec = "6h";
+                RandomizedDelaySec = "6h";
+                Persistent = true;
+              };
+            };
+          }
+      )
+      reposToMirror;
+
+    systemd.services = lib.mapAttrs'
+      (
+        name: repo:
+          {
+            name = unitName name;
+            value = {
+              description = "mirror git repository ${name}";
+              after = [ "network.target" ];
+              script =
+                let
+                  path = repoPath name repo;
+                in
+                ''
+                  set -euo pipefail
+
+                  export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.git ]}"
+
+                  if test ! -d "${path}"; then
+                    mkdir -p "$(dirname "${path}")"
+                    git clone --mirror "${repo.upstream}" "${path}"
+                    exit 0
+                  fi
+
+                  cd "${path}"
+
+                  git fetch "${repo.upstream}" '+refs/*:refs/*' --prune
+                '';
+
+              serviceConfig = {
+                Type = "oneshot";
+                User = mirroredReposOwner;
+                Group = mirroredReposOwner;
+              };
+            };
+          }
+      )
+      reposToMirror;
+  };
+}
diff --git a/users/sterni/machines/edwin/http/fcgiwrap.nix b/users/sterni/machines/edwin/http/fcgiwrap.nix
new file mode 100644
index 000000000000..19696d85d413
--- /dev/null
+++ b/users/sterni/machines/edwin/http/fcgiwrap.nix
@@ -0,0 +1,15 @@
+{ ... }:
+
+{
+  imports = [
+    ./nginx.nix
+  ];
+
+  config.services.fcgiwrap = {
+    enable = true;
+    socketType = "unix";
+    socketAddress = "/run/fcgiwrap.sock";
+    user = "http";
+    group = "http";
+  };
+}
diff --git a/users/sterni/machines/edwin/http/flipdot.openlab-augsburg.de.nix b/users/sterni/machines/edwin/http/flipdot.openlab-augsburg.de.nix
new file mode 100644
index 000000000000..c86956a0a473
--- /dev/null
+++ b/users/sterni/machines/edwin/http/flipdot.openlab-augsburg.de.nix
@@ -0,0 +1,36 @@
+{ depot, lib, config, ... }:
+
+let
+  inherit (depot.users.sterni.external.flipdot-gschichtler)
+    bahnhofshalle
+    warteraum
+    nixosModule
+    ;
+in
+
+{
+  imports = [
+    nixosModule
+    ./nginx.nix
+  ];
+
+  config = {
+    age.secrets = lib.genAttrs [
+      "warteraum-salt"
+      "warteraum-tokens"
+    ]
+      (name: {
+        file = depot.users.sterni.secrets."${name}.age";
+      });
+
+    services.flipdot-gschichtler = {
+      enable = true;
+      virtualHost = "flipdot.openlab-augsburg.de";
+      packages = {
+        inherit bahnhofshalle warteraum;
+      };
+      saltFile = config.age.secretsDir + "/warteraum-salt";
+      tokensFile = config.age.secretsDir + "/warteraum-tokens";
+    };
+  };
+}
diff --git a/users/sterni/machines/edwin/http/likely-music.sterni.lv.nix b/users/sterni/machines/edwin/http/likely-music.sterni.lv.nix
new file mode 100644
index 000000000000..8da03ac5e6ec
--- /dev/null
+++ b/users/sterni/machines/edwin/http/likely-music.sterni.lv.nix
@@ -0,0 +1,23 @@
+{ depot, ... }:
+
+let
+  inherit (depot.users.sterni.external.likely-music)
+    nixosModule
+    likely-music
+    ;
+in
+
+{
+  imports = [
+    ./nginx.nix
+    nixosModule
+  ];
+
+  config = {
+    services.likely-music = {
+      enable = true;
+      virtualHost = "likely-music.sterni.lv";
+      package = likely-music;
+    };
+  };
+}
diff --git a/users/sterni/machines/edwin/http/nginx.nix b/users/sterni/machines/edwin/http/nginx.nix
new file mode 100644
index 000000000000..7c99cdd150e0
--- /dev/null
+++ b/users/sterni/machines/edwin/http/nginx.nix
@@ -0,0 +1,28 @@
+{ ... }:
+
+{
+  config = {
+    users = {
+      users.http = {
+        isSystemUser = true;
+        group = "http";
+      };
+
+      groups.http = { };
+    };
+
+    services.nginx = {
+      enable = true;
+      recommendedTlsSettings = true;
+      recommendedGzipSettings = true;
+      recommendedProxySettings = true;
+
+      user = "http";
+      group = "http";
+
+      appendHttpConfig = ''
+        charset utf-8;
+      '';
+    };
+  };
+}
diff --git a/users/sterni/machines/edwin/http/sterni.lv.nix b/users/sterni/machines/edwin/http/sterni.lv.nix
new file mode 100644
index 000000000000..44306c75bf64
--- /dev/null
+++ b/users/sterni/machines/edwin/http/sterni.lv.nix
@@ -0,0 +1,16 @@
+{ ... }:
+
+{
+  imports = [
+    ./nginx.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."sterni.lv" = {
+      enableACME = true;
+      forceSSL = true;
+      # TODO(sterni): take website from store, replace /tmp with a simple LRU thing
+      root = toString /srv/http;
+    };
+  };
+}
diff --git a/users/sterni/machines/edwin/minecraft.nix b/users/sterni/machines/edwin/minecraft.nix
new file mode 100644
index 000000000000..1d623f00e663
--- /dev/null
+++ b/users/sterni/machines/edwin/minecraft.nix
@@ -0,0 +1,121 @@
+{ pkgs, depot, config, ... }:
+
+let
+  carpet = pkgs.fetchurl {
+    url = "https://github.com/gnembon/fabric-carpet/releases/download/1.4.101/fabric-carpet-1.19.4-1.4.101+v230319.jar";
+    sha256 = "1zppsl3x9iaj616phrllc8hirj4f5wqdjf6f9w2nm0mkxr66z10l";
+  };
+
+  carpet-extra = pkgs.fetchurl {
+    url = "https://github.com/gnembon/carpet-extra/releases/download/1.4.100/carpet-extra-1.19.4-1.4.100.jar";
+    sha256 = "1x3jh7nds5kkda445sbcgnz5fvw42f4pq0pvarz7rf9wgkz15i8r";
+  };
+
+  userGroup = "minecraft";
+
+  makeJvmOpts = megs: [
+    "-Xms${toString megs}M"
+    "-Xmx${toString megs}M"
+  ];
+
+  whitelist = {
+    spreadwasser = "242a66eb-2df2-4585-9a28-ac763ad0d0f9";
+    sternenseemann = "d8e48069-1905-4886-a5da-a4ee917ee254";
+  };
+
+  rconPasswordFile = config.age.secretsDir + "/minecraft-rcon";
+
+  baseProperties = {
+    white-list = true;
+    allow-flight = true;
+    difficulty = "hard";
+    function-permission-level = 4;
+    snooper-enabled = false;
+    view-distance = 12;
+    sync-chunk-writes = "false"; # the single biggest performance fix
+    max-tick-time = 6000000; # TODO(sterni): disable watchdog via carpet
+    enforce-secure-profile = false;
+  };
+in
+
+# TODO(sterni): regular backups of carpet world
+
+{
+  imports = [
+    ../../modules/minecraft-fabric.nix
+  ];
+
+  config = {
+    environment.systemPackages = [
+      pkgs.mcrcon
+      pkgs.jre
+    ];
+
+    users = {
+      users."${userGroup}" = {
+        isNormalUser = true;
+        openssh.authorizedKeys.keys = depot.users.sterni.keys.all;
+        shell = "${pkgs.fish}/bin/fish";
+      };
+
+      groups."${userGroup}" = { };
+    };
+
+    age.secrets = {
+      minecraft-rcon.file = depot.users.sterni.secrets."minecraft-rcon.age";
+    };
+
+    services.minecraft-fabric-server = {
+      creative = {
+        enable = true;
+        version = "1.19.4";
+        mods = [
+          carpet
+          carpet-extra
+        ];
+        world = config.users.users.${userGroup}.home + "/worlds/creative";
+
+        jvmOpts = makeJvmOpts 2048;
+        user = userGroup;
+        group = userGroup;
+
+        inherit whitelist rconPasswordFile;
+        ops = whitelist;
+
+        serverProperties = baseProperties // {
+          server-port = 25566;
+          "rcon.port" = 25576;
+          gamemode = "creative";
+          enable-command-block = true;
+          motd = "storage design server";
+          spawn-protection = 2;
+        };
+      };
+
+      carpet = {
+        enable = true;
+        version = "1.19.4";
+        mods = [
+          carpet
+          carpet-extra
+        ];
+        world = config.users.users.${userGroup}.home + "/worlds/carpet";
+
+        jvmOpts = makeJvmOpts 4096;
+        user = userGroup;
+        group = userGroup;
+
+        inherit whitelist rconPasswordFile;
+        ops = whitelist;
+
+        serverProperties = baseProperties // {
+          server-port = 25565;
+          "rcon.port" = 25575;
+          motd = "ich tu fleissig hustlen nenn mich bob der baumeister";
+
+          level-seed = 7240251176989694927; # for posterity
+        };
+      };
+    };
+  };
+}
diff --git a/users/sterni/machines/edwin/network.nix b/users/sterni/machines/edwin/network.nix
new file mode 100644
index 000000000000..1e3d4e76f078
--- /dev/null
+++ b/users/sterni/machines/edwin/network.nix
@@ -0,0 +1,62 @@
+{ config, pkgs, lib, depot, ... }:
+
+let
+  ipv6 = "2a01:4f8:151:54d0::/64";
+
+  ipv4 = "176.9.107.207";
+  gatewayv4 = "176.9.107.193";
+  netmaskv4 = "255.255.255.224";
+in
+
+{
+  config = {
+    boot = {
+      kernelParams = [
+        "ip=${ipv4}::${gatewayv4}:${netmaskv4}::eth0:none"
+      ];
+
+      initrd.network = {
+        enable = true;
+        ssh = {
+          enable = true;
+          authorizedKeys = depot.users.sterni.keys.all;
+          hostKeys = [
+            "/etc/nixos/unlock_rsa_key_openssh"
+            "/etc/nixos/unlock_ed25519_key_openssh"
+          ];
+        };
+        postCommands = ''
+          echo 'cryptsetup-askpass' >> /root/.profile
+        '';
+      };
+    };
+
+    networking = {
+      usePredictableInterfaceNames = false;
+      useDHCP = false;
+      interfaces."eth0".useDHCP = false;
+
+      hostName = "edwin";
+
+      firewall = {
+        enable = true;
+        allowPing = true;
+        allowedTCPPorts = [ 22 80 443 ];
+      };
+    };
+
+    systemd.network = {
+      enable = true;
+      networks."eth0".extraConfig = ''
+        [Match]
+        Name = eth0
+
+        [Network]
+        Address = ${ipv6}
+        Gateway = fe80::1
+        Address = ${ipv4}/27
+        Gateway = ${gatewayv4}
+      '';
+    };
+  };
+}
diff --git a/users/sterni/mblog/.gitignore b/users/sterni/mblog/.gitignore
new file mode 100644
index 000000000000..ae957fcad0dc
--- /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/LICENSE b/users/sterni/mblog/LICENSE
new file mode 100644
index 000000000000..f288702d2fa1
--- /dev/null
+++ b/users/sterni/mblog/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/users/sterni/mblog/cli.lisp b/users/sterni/mblog/cli.lisp
new file mode 100644
index 000000000000..555f8def535f
--- /dev/null
+++ b/users/sterni/mblog/cli.lisp
@@ -0,0 +1,75 @@
+;; SPDX-License-Identifier: GPL-3.0-only
+;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni
+
+(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."
+  (config:init-from-env)
+  (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/config.lisp b/users/sterni/mblog/config.lisp
new file mode 100644
index 000000000000..0d4cbfe8ae20
--- /dev/null
+++ b/users/sterni/mblog/config.lisp
@@ -0,0 +1,31 @@
+;; SPDX-License-Identifier: GPL-3.0-only
+;; SPDX-FileCopyrightText: Copyright (C) 2023 by sterni
+
+(in-package :config)
+
+(eval-when (:compile-toplevel :load-toplevel)
+  (defun plist-to-alist (lst)
+    (loop for (name . (default . (parser . nil))) on lst by #'cdddr
+          collect (cons name (list default parser))))
+
+  (defun symbol-to-env-var-name (symbol)
+    (concatenate 'string
+                 "MBLOG_"
+                 (string-upcase
+                  (remove #\* (substitute #\_ #\- (string symbol)))))))
+
+(defmacro define-configuration-variables (&rest args)
+  (let ((vars (plist-to-alist args))
+        (val-var-sym (gensym)))
+    `(progn
+       ,@(loop for (name . (default nil)) in vars
+              collect `(defvar ,name ,default))
+
+       (defun init-from-env ()
+         ,@(loop for (name . (nil parser)) in vars
+                 collect
+                 `(when-let ((,val-var-sym (getenv ,(symbol-to-env-var-name name))))
+                    (setf ,name (funcall ,parser ,val-var-sym))))))))
+
+(define-configuration-variables
+  *general-buffer-size* (min 4096 qbase64:+max-bytes-length+) #'parse-integer)
diff --git a/users/sterni/mblog/default.nix b/users/sterni/mblog/default.nix
new file mode 100644
index 000000000000..6ad8a10ce378
--- /dev/null
+++ b/users/sterni/mblog/default.nix
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: GPL-3.0-only
+# SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni
+{ depot, pkgs, ... }:
+
+(depot.nix.buildLisp.program {
+  name = "mblog";
+
+  srcs = [
+    ./packages.lisp
+    ./config.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 000000000000..42f18c619d18
--- /dev/null
+++ b/users/sterni/mblog/maildir.lisp
@@ -0,0 +1,20 @@
+;; SPDX-License-Identifier: GPL-3.0-only
+;; SPDX-FileCopyrightText: Copyright (C) 2022 by sterni
+
+(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 000000000000..7823bde20343
--- /dev/null
+++ b/users/sterni/mblog/mblog.lisp
@@ -0,0 +1,147 @@
+;; SPDX-License-Identifier: GPL-3.0-only
+;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni
+;; SPDX-FileCopyrightText: Copyright (C) 2006-2010 by Walter C. Pelissero
+
+(in-package :mblog)
+
+;; util
+
+;; Taken from SCLF, written by Walter C. Pelissero
+(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))
+
+(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))
+
+;; 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")))
+     (: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
+                           :buffer-size *general-buffer-size*)))))
+
+  (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 000000000000..f056aaa72d54
--- /dev/null
+++ b/users/sterni/mblog/note.lisp
@@ -0,0 +1,118 @@
+;; SPDX-License-Identifier: GPL-3.0-only
+;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni
+
+(in-package :note)
+(declaim (optimize (safety 3)))
+
+;;; util
+
+(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 config:*general-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) 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 000000000000..d6e33955d31c
--- /dev/null
+++ b/users/sterni/mblog/packages.lisp
@@ -0,0 +1,64 @@
+;; SPDX-License-Identifier: GPL-3.0-only
+;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni
+
+(defpackage :maildir
+  (:use :common-lisp)
+  (:shadow :list)
+  (:export :list)
+  (:documentation
+   "Very incomplete package for dealing with maildir(5)."))
+
+(defpackage :config
+  (:use
+   :common-lisp)
+  (:import-from :uiop :getenv)
+  (:import-from :alexandria :when-let)
+  (:export
+   :init-from-env
+   :*general-buffer-size*))
+
+(defpackage :note
+  (:use
+   :common-lisp
+   :closure-html
+   :cl-date-time-parser
+   :mime4cl
+   :config)
+  (: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
+   :config)
+  (:export :build-mblog)
+  (:import-from :local-time :universal-to-timestamp)
+  (:import-from :mime4cl :redirect-stream)
+  (:shadowing-import-from :common-lisp :list))
+
+(defpackage :cli
+  (:use
+   :common-lisp
+   :uiop
+   :note
+   :config
+   :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 000000000000..c499eafbec4f
--- /dev/null
+++ b/users/sterni/mblog/transformer.lisp
@@ -0,0 +1,130 @@
+;; SPDX-License-Identifier: GPL-3.0-only
+;; SPDX-FileCopyrightText: Copyright (C) 2022 by sterni
+
+(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/modules/default.nix b/users/sterni/modules/default.nix
new file mode 100644
index 000000000000..5cc8be3cc6ce
--- /dev/null
+++ b/users/sterni/modules/default.nix
@@ -0,0 +1,2 @@
+# Stop readTree from looking at this directory
+_: { }
diff --git a/users/sterni/modules/disk-checkup.nix b/users/sterni/modules/disk-checkup.nix
new file mode 100644
index 000000000000..8acc08759b17
--- /dev/null
+++ b/users/sterni/modules/disk-checkup.nix
@@ -0,0 +1,25 @@
+# TODO(sterni): configure smartd and alerts
+{ config, lib, ... }:
+
+{
+  config = {
+    services = {
+      btrfs.autoScrub = {
+        enable = true;
+        interval = "daily";
+        # gather all btrfs fileSystems and overwrite default
+        fileSystems = lib.mkForce (
+          lib.concatLists (
+            lib.mapAttrsToList
+              (
+                _:
+                { fsType, mountPoint, ... }:
+                if fsType == "btrfs" then [ mountPoint ] else [ ]
+              )
+              config.fileSystems
+          )
+        );
+      };
+    };
+  };
+}
diff --git a/users/sterni/modules/minecraft-fabric.nix b/users/sterni/modules/minecraft-fabric.nix
new file mode 100644
index 000000000000..7c04aae725dd
--- /dev/null
+++ b/users/sterni/modules/minecraft-fabric.nix
@@ -0,0 +1,497 @@
+# Declarative, but low Nix module for a modded minecraft server using the
+# fabric mod loader. That is to say, the build of the final server JAR
+# is not encapsulated in a derivation.
+#
+# The module has the following interesting properties:
+#
+#   * The fabric installer is executed on each server startup to assemble the
+#     patched server.jar. This is unfortunately necessary, as it seems to be
+#     difficult to do so in a derivation (fabric-installer accesses the network,
+#     the build doesn't seem to be reproducible). At least this avoids the
+#     question of the patched jar's redistributability.
+#   * RCON is used for starting and stopping which should prevent data loss,
+#     since we can issue a manual save command.
+#   * The entire runtime directory of the server is assembled from scratch on
+#     each start, so only blessed state (like the world) and declarative
+#     configuration (whitelist.json, server.properties, ...) survive.
+#   * It supports more than one server running on the same machine.
+#
+# Missing features:
+#
+#   * Support for bans
+#   * Support for mutable whitelist, ops, …
+#   * Op levels
+#
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2022-2023 sterni <sternenseemann@systemli.org>
+
+{ lib, pkgs, config, depot, ... }:
+
+let
+  #
+  # Dependencies
+  #
+  inherit (depot.nix.utils) storePathName;
+  inherit (depot.nix) getBins;
+
+  bins = getBins pkgs.mcrcon [ "mcrcon" ]
+    // getBins pkgs.jre [ "java" ]
+    // getBins pkgs.diffutils [ "diff" ]
+    // getBins pkgs.moreutils [ "sponge" ]
+    // getBins pkgs.extrace [ "pwait" ]
+    // getBins pkgs.util-linux [ "flock" ];
+
+  #
+  # Needed JARs
+  #
+  fetchJar = { pname, version, url, sha256, passthru ? { } }:
+    pkgs.fetchurl {
+      name = "${pname}-${version}.jar";
+      inherit url sha256;
+      passthru = passthru // { inherit version; };
+    };
+
+  fabricInstallerJar =
+    fetchJar rec {
+      pname = "fabric-installer";
+      version = "0.11.2";
+      url = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/${version}/fabric-installer-${version}.jar";
+      sha256 = "09sw013b385cw1w4n4f89bgsy1n9q90fagmy4xr5laxi3gpmpbf6";
+    };
+
+  # log4j workaround for Minecraft Server >= 1.12 && < 1.17
+  log4jFix_112_116 = pkgs.fetchurl {
+    url = "https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml";
+    sha256 = "1paha357xbaffl38ckzgdh4l5iib2ydqbv7jsg67nj31nlalclr9";
+  };
+
+  serverJars = {
+    # Manually updated list of known minecraft `server.jar`s for now.
+    # Making this comprehensive isn't that interesting for now, since the module
+    # is annoying to use outside of depot anyways as it uses //nix.
+    "1.16.5" = fetchJar {
+      pname = "server";
+      version = "1.16.5";
+      url = "https://launcher.mojang.com/v1/objects/1b557e7b033b583cd9f66746b7a9ab1ec1673ced/server.jar";
+      sha256 = "19ix6x5ij4jcwqam1dscnqwm0m251gysc2j793wjcrb9sb3jkwsq";
+      passthru = {
+        baseJvmOpts = [
+          "-Dlog4j.configurationFile=${log4jFix_112_116}"
+        ];
+      };
+    };
+    "1.17" = fetchJar {
+      pname = "server";
+      version = "1.17";
+      url = "https://launcher.mojang.com/v1/objects/0a269b5f2c5b93b1712d0f5dc43b6182b9ab254e/server.jar";
+      sha256 = "0jqz7hpx7zvjj2n5rfrh8jmdj6ziqyp8c9nq4sr4jmkbky6hsfbv";
+      passthru.baseJvmOpts = [
+        "-Dlog4j2.formatMsgNoLookups=true"
+      ];
+    };
+    "1.17.1" = fetchJar {
+      pname = "server";
+      version = "1.17.1";
+      url = "https://launcher.mojang.com/v1/objects/a16d67e5807f57fc4e550299cf20226194497dc2/server.jar";
+      sha256 = "0pzmzagvrrapjsnd8xg4lqwynwnb5rcqk2n9h2kzba8p2fs13hp8";
+      passthru.baseJvmOpts = [
+        "-Dlog4j2.formatMsgNoLookups=true"
+      ];
+    };
+    "1.18" = fetchJar {
+      pname = "server";
+      version = "1.18";
+      url = "https://launcher.mojang.com/v1/objects/3cf24a8694aca6267883b17d934efacc5e44440d/server.jar";
+      sha256 = "0vvycjcfq96z7cl5dsrq98k9b7j7l4x0y9nflrcqmcvink7fs5w4";
+      passthru.baseJvmOpts = [
+        "-Dlog4j2.formatMsgNoLookups=true"
+      ];
+    };
+    "1.18.1" = fetchJar {
+      pname = "server";
+      version = "1.18.1";
+      url = "https://launcher.mojang.com/v1/objects/125e5adf40c659fd3bce3e66e67a16bb49ecc1b9/server.jar";
+      sha256 = "1pyvym6xzjb1siizzj4ma7lpb05qhgxnzps8lmlbk00lv0515kgb";
+    };
+    "1.18.2" = fetchJar {
+      pname = "server";
+      version = "1.18.2";
+      url = "https://launcher.mojang.com/v1/objects/c8f83c5655308435b3dcf03c06d9fe8740a77469/server.jar";
+      sha256 = "0hx330bnadixph44sip0h5h986m11qxbdba6lbgwz4da6lg9vgjp";
+    };
+    "1.19" = fetchJar {
+      pname = "server";
+      version = "1.19";
+      url = "https://launcher.mojang.com/v1/objects/e00c4052dac1d59a1188b2aa9d5a87113aaf1122/server.jar";
+      sha256 = "1cnjrqr2vn8gppd1y1lcdrc46fd7m1b3zl28zpbw72fgy1bd1vyy";
+    };
+    "1.19.1" = fetchJar {
+      pname = "server";
+      version = "1.19.1";
+      url = "https://piston-data.mojang.com/v1/objects/8399e1211e95faa421c1507b322dbeae86d604df/server.jar";
+      sha256 = "0jnlb5z8a7qi6p6bbwnmdl77b8kq83ryfdp58dhx8kg2hf6lbfx8";
+    };
+    "1.19.2" = fetchJar {
+      pname = "server";
+      version = "1.19.2";
+      url = "https://piston-data.mojang.com/v1/objects/f69c284232d7c7580bd89a5a4931c3581eae1378/server.jar";
+      sha256 = "15jdxh5zvsgvvk9hnv47swgjfg8fr653g6nx99q1rxpmkq32frxj";
+    };
+    "1.19.3" = fetchJar {
+      pname = "server";
+      version = "1.19.3";
+      url = "https://piston-data.mojang.com/v1/objects/c9df48efed58511cdd0213c56b9013a7b5c9ac1f/server.jar";
+      sha256 = "06qykz3nq7qmfw4phs3wvq3nk28clg8s3qrs37856aai8b8kmgaf";
+    };
+    # Starting with 1.19.4 we could use --pidFile for systemd's PIDFile=, but as
+    # the service doesn't fork, there seems to be no point.
+    "1.19.4" = fetchJar {
+      pname = "server";
+      version = "1.19.4";
+      url = "https://piston-data.mojang.com/v1/objects/8f3112a1049751cc472ec13e397eade5336ca7ae/server.jar";
+      sha256 = "0lrzpqd6zjvqh9g2byicgh66n43z0hwzp863r22ifx2hll6s2955";
+    };
+  };
+
+  #
+  # mods directory for fabric
+  #
+  makeModFolder = name: mods:
+    pkgs.runCommand "${name}-fabric-mod-folder" { } (
+      ''
+        mkdir -p "$out"
+      '' + lib.concatMapStrings
+        (mod: ''
+          test -f "${mod}" || {
+              printf 'Not a regular file: %s\n' "${mod}" >&2
+              exit 1
+          }
+          ln -s "${mod}" "$out/${storePathName mod}"
+        '')
+        mods
+    );
+
+  #
+  # Create a server.properties file
+  #
+  propertyValue = v:
+    if builtins.isBool v
+    then lib.boolToString v
+    else toString v;
+
+  serverPropertiesFile = name: instanceCfg:
+    let
+      serverProperties' =
+        builtins.removeAttrs instanceCfg.serverProperties [
+          "rcon.password"
+        ] // {
+          enable-rcon = true;
+        };
+    in
+    pkgs.writeText "${name}-server.properties" (''
+      # created by minecraft-fabric.nix
+    '' + lib.concatStrings (lib.mapAttrsToList
+      (key: value: ''
+        ${key}=${propertyValue value}
+      '')
+      serverProperties'));
+
+  #
+  # Create JSON “state” files
+  #
+  writeJson = name: data: pkgs.writeText "${name}.json" (builtins.toJSON data);
+
+  toWhitelist = name: uuid: { inherit name uuid; };
+
+  whitelistFile = name: instanceCfg:
+    writeJson "${name}-whitelist" (
+      lib.mapAttrsToList toWhitelist instanceCfg.whitelist
+    );
+
+  opsFile = name: instanceCfg:
+    writeJson "${name}-ops" (
+      lib.mapAttrsToList
+        (name: value:
+          toWhitelist name value // {
+            level = 4;
+            bypassesPlayerLimit = true;
+          }
+        )
+        instanceCfg.ops
+    );
+
+  #
+  # Service start and stop scripts
+  #
+  stopScript = name: instanceCfg:
+    pkgs.writeShellScript "minecraft-fabric-${name}-stop" ''
+      set -eu
+
+      # Before shutting down, display the diff between prescribed and used
+      # server.properties file for debugging purposes; filter out credential
+      actualProperties="''${RUNTIME_DIRECTORY}/server.properties"
+      sort "$actualProperties" | ${bins.sponge} "$actualProperties"
+      ( ${bins.diff} -u "${serverPropertiesFile name instanceCfg}" \
+          "$actualProperties" \
+          || true ) | grep -v rcon.password
+
+      export MCRCON_HOST=localhost
+      export MCRCON_PORT=${lib.escapeShellArg instanceCfg.serverProperties."rcon.port"}
+      # Unfortunately, mcrcon can't read the password from a file
+      export MCRCON_PASS="$(cat "''${CREDENTIALS_DIRECTORY}/rcon-password")"
+
+      # Send stop request
+      "${bins.mcrcon}" 'say Server is stopping' save-all stop
+
+      # Wait for service to come down (systemd SIGTERMs right after ExecStop)
+      "${bins.flock}" "''${RUNTIME_DIRECTORY}" true
+    '';
+
+  startScript = name: instanceCfg:
+    let
+      serverJar = serverJars.${instanceCfg.version} or
+        (throw "Don't have server.jar for Minecraft Server ${instanceCfg.version}");
+
+    in
+
+    pkgs.writeShellScript "minecraft-fabric-${name}-start" ''
+      set -eu
+
+      cd "''${RUNTIME_DIRECTORY}"
+
+      copyFromStore() {
+          install -m600 "$1" "$2"
+      }
+
+      # Check if world is available
+      if test ! -d "${instanceCfg.world}"; then
+          echo "Could not find world, generating new one" >&2
+          mkdir -p "${instanceCfg.world}"
+      fi
+
+      # Put required files into place
+      echo eula=true > eula.txt
+      ln -s "${instanceCfg.world}" "${instanceCfg.level-name or "world"}"
+      copyFromStore "${serverJar}" server.jar
+      copyFromStore "${whitelistFile name instanceCfg}" whitelist.json
+      copyFromStore "${opsFile name instanceCfg}" ops.json
+      ln -s "${makeModFolder name instanceCfg.mods}" mods
+
+      # Create config and set password from credentials (echo hopefully doesn't leak)
+      copyFromStore "${serverPropertiesFile name instanceCfg}" server.properties
+      echo "rcon.password=$(cat "$CREDENTIALS_DIRECTORY/rcon-password")" >> server.properties
+
+      # Build patched jar
+      "${bins.java}" -jar "${fabricInstallerJar}" \
+          server -mcversion "${instanceCfg.version}"
+
+      # Lock is held as long as the server is running, so that we can wait for
+      # the actual shutdown in the stop script without relying on $MAINPID.
+      exec "${bins.flock}" "''${RUNTIME_DIRECTORY}" \
+          "${bins.java}" \
+          ${lib.escapeShellArgs (serverJar.baseJvmOpts or [ ] ++ instanceCfg.jvmOpts)} \
+          -jar fabric-server-launch.jar nogui
+    '';
+
+  #
+  # Option types
+  #
+  impurePath = lib.types.path // {
+    name = "impurePath";
+    check = x:
+      lib.types.path.check x
+        && !(builtins.isPath x)
+        && !(lib.hasPrefix builtins.storeDir (toString x));
+  };
+
+
+  instanceType = lib.types.submodule {
+    options = {
+      enable = lib.mkEnableOption "Minecraft server instance with the fabric mod loader";
+
+      version = lib.mkOption {
+        type = lib.types.str;
+        description = "Minecraft Server version to use.";
+        example = "1.16.5";
+      };
+
+      mods = lib.mkOption {
+        type = with lib.types; listOf package;
+        description = "List of fabric mod JARs to load.";
+        default = [ ];
+      };
+
+      world = lib.mkOption {
+        type = impurePath;
+        description = "Path to the Minecraft world folder to use.";
+        example = "/var/minecraft/world";
+      };
+
+      jvmOpts = lib.mkOption {
+        type = with lib.types; listOf str;
+        default = [ ];
+        example = [
+          "-Xmx2048M"
+          "-Xms2048M"
+        ];
+        description = ''
+          Options to pass to
+          <citerefentry>
+            <refentrytitle>java</refentrytitle>
+            <manvolnum>1</manvolnum>
+          </citerefentry>
+          in order to tweak the runtime of the JVM.
+        '';
+      };
+
+      user = lib.mkOption {
+        type = lib.types.str;
+        default = "minecraft";
+        description = ''
+          Name of an existing user to run the server as. Needs to have write
+          access to the specified world.
+        '';
+      };
+
+      group = lib.mkOption {
+        type = lib.types.str;
+        default = "users";
+        description = ''
+          Name of an existing group to run the server under.
+        '';
+      };
+
+      rconPasswordFile = lib.mkOption {
+        type = impurePath;
+        description = ''
+          File (outised the store) that stores the password to use for Minecraft's
+          RCON interface.
+        '';
+        example = "/var/secrets/minecraft-rcon";
+      };
+
+      whitelist = lib.mkOption {
+        type = with lib.types; attrsOf str;
+        description = ''
+          Attribute set mapping whitelisted user names to their user ids.
+        '';
+        example = {
+          sternenseemann = "d8e48069-1905-4886-a5da-a4ee917ee254";
+        };
+      };
+
+      ops = lib.mkOption {
+        type = with lib.types; attrsOf str;
+        description = ''
+          Attribute set mapping op-ed user names to their user ids.
+          Setting permission levels is not possible at the moment,
+          set to 4 by default.
+        '';
+        example = {
+          sternenseemann = "d8e48069-1905-4886-a5da-a4ee917ee254";
+        };
+      };
+
+      serverProperties = lib.mkOption {
+        type = lib.types.submodule {
+          freeformType = lib.types.attrs;
+
+          # Only options the module needs to access are declared explicitly
+          options = {
+            server-port = lib.mkOption {
+              type = lib.types.port;
+              default = 25565;
+              description = ''
+                Port to listen on.
+              '';
+            };
+
+            "rcon.port" = lib.mkOption {
+              type = lib.types.port;
+              default = 25575;
+              description = ''
+                Port to use for the RCON control mechanism.
+              '';
+            };
+          };
+        };
+      };
+    };
+  };
+
+  cfg = config.services.minecraft-fabric-server;
+
+  serverPorts = lib.mapAttrsToList
+    (_: instanceCfg:
+      instanceCfg.serverProperties.server-port
+    )
+    cfg;
+
+  rconPorts = lib.mapAttrsToList
+    (_: instanceCfg:
+      instanceCfg.serverProperties."rcon.port"
+    )
+    cfg;
+in
+
+{
+  options = {
+    services.minecraft-fabric-server = lib.mkOption {
+      type = with lib.types; attrsOf instanceType;
+      default = { };
+      description = "Minecraft server instances with the fabric mod loader";
+    };
+  };
+
+  config = {
+    assertions = [
+      {
+        assertion = builtins.all (instance: !instance.enable) (builtins.attrValues cfg)
+          || config.nixpkgs.config.allowUnfreeRedistributable or false
+          || config.nixpkgs.config.allowUnfree or false;
+        message = lib.concatStringsSep " " [
+          "You need to allow unfree software for minecraft,"
+          "as you'll implicitly agree to Mojang's EULA."
+        ];
+      }
+      {
+        assertion =
+          let
+            allPorts = serverPorts ++ rconPorts;
+          in
+          lib.unique allPorts == allPorts;
+        message = "All assigned ports need to be unique.";
+      }
+    ];
+
+    systemd.services = lib.mapAttrs'
+      (name: instanceCfg:
+        {
+          name = "minecraft-fabric-${name}";
+          inherit (instanceCfg) enable;
+          value = {
+            description = "Minecraft server ${name} with the fabric mod loader";
+            wantedBy = [ "multi-user.target" ];
+            after = [ "network.target" ];
+
+            serviceConfig = {
+              Type = "simple";
+              User = instanceCfg.user;
+              Group = instanceCfg.group;
+              ExecStart = startScript name instanceCfg;
+              ExecStop = stopScript name instanceCfg;
+              RuntimeDirectory = "minecraft-fabric-${name}";
+              LoadCredential = "rcon-password:${instanceCfg.rconPasswordFile}";
+              RestartSec = "40s";
+            };
+          };
+        }
+      )
+      cfg;
+
+    networking.firewall = {
+      allowedTCPPorts = serverPorts;
+      allowedUDPPorts = serverPorts;
+    };
+  };
+}
diff --git a/users/sterni/nix/build/buildGopherHole/default.nix b/users/sterni/nix/build/buildGopherHole/default.nix
new file mode 100644
index 000000000000..eec13a865421
--- /dev/null
+++ b/users/sterni/nix/build/buildGopherHole/default.nix
@@ -0,0 +1,109 @@
+{ depot, pkgs, lib, ... }:
+
+let
+  inherit (pkgs)
+    runCommand
+    writeText
+    ;
+
+  inherit (depot.users.sterni.nix.build)
+    buildGopherHole
+    ;
+
+  fileTypes = {
+    # RFC1436
+    text = "0";
+    menu = "1";
+    cso = "2";
+    error = "3";
+    binhex = "4";
+    dos = "5";
+    uuencoded = "6";
+    index-server = "7";
+    telnet = "8";
+    binary = "9";
+    mirror = "+";
+    gif = "g";
+    image = "I";
+    tn3270 = "T";
+    # non-standard
+    info = "i";
+    html = "h";
+  };
+
+  buildFile = { file, name, fileType ? fileTypes.text }:
+    runCommand name
+      {
+        passthru = {
+          # respect the file type the file derivation passes
+          # through. otherwise use explicitly set type or
+          # default value.
+          fileType = file.fileType or fileType;
+        };
+      } ''
+      ln -s ${file} "$out"
+    '';
+
+  buildGopherMap = dir:
+    let
+      /* strings constitute an info line or an empty line
+         if their length is zero. sets that contain a menu
+         value have that added to the gophermap as-is.
+
+         all other entries should be a set which can be built using
+         buildGopherHole and is linked by their name. The resulting
+         derivation is expected to passthru a fileType containing the
+         gopher file type char of themselves.
+      */
+      gopherMapLine = e:
+        if builtins.isString e
+        then e
+        else if e ? menu
+        then e.menu
+        else
+          let
+            drv = buildGopherHole e;
+            title = e.title or e.name;
+          in
+          "${drv.fileType}${title}\t${drv.name}";
+    in
+    writeText ".gophermap" (lib.concatMapStringsSep "\n" gopherMapLine dir);
+
+  buildDir =
+    { dir, name, ... }:
+
+    let
+      # filter all entries out that have to be symlinked:
+      # sets with the file or dir attribute
+      drvOnly = builtins.map buildGopherHole (builtins.filter
+        (x: !(builtins.isString x) && (x ? dir || x ? file))
+        dir);
+      gopherMap = buildGopherMap dir;
+    in
+    runCommand name
+      {
+        passthru = {
+          fileType = fileTypes.dir;
+        };
+      }
+      (''
+        mkdir -p "$out"
+        ln -s "${gopherMap}" "$out/.gophermap"
+      '' + lib.concatMapStrings
+        (drv: ''
+          ln -s "${drv}" "$out/${drv.name}"
+        '')
+        drvOnly);
+in
+
+{
+  # Dispatch into different file / dir handling code
+  # which is mutually recursive with this function.
+  __functor = _: args:
+    if args ? file then buildFile args
+    else if args ? dir then buildDir args
+    else builtins.throw "Unrecognized gopher hole item type: "
+      + lib.generators.toPretty { } args;
+
+  inherit fileTypes;
+}
diff --git a/users/sterni/nix/char/all-chars.bin b/users/sterni/nix/char/all-chars.bin
new file mode 100644
index 000000000000..017b909e8e8e
--- /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 000000000000..9c6ce2fb250b
--- /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 000000000000..313df474514c
--- /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 000000000000..4bef0abb91e9
--- /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 000000000000..9f974a61c7b2
--- /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 000000000000..824cebfed244
--- /dev/null
+++ b/users/sterni/nix/fun/default.nix
@@ -0,0 +1,255 @@
+{ 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;
+                final = false;
+                inherit args;
+              }
+            ];
+
+            operator =
+              { key, final, ... }@state:
+              let
+                # Plumbing to make genericClosure happy
+                newId = {
+                  key = key + 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 newId // {
+                    final = false;
+                    inherit (call) args;
+                  } else newId // {
+                    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 000000000000..6b1e6fcc7b0b
--- /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 000000000000..0349e466a166
--- /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 000000000000..d25a7ab8dac0
--- /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 000000000000..ed520675c55a
--- /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.runCommand "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 000000000000..a39a9477e211
--- /dev/null
+++ b/users/sterni/nix/int/default.nix
@@ -0,0 +1,129 @@
+{ 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;
+
+  sum = builtins.foldl' (a: b: a + b) 0;
+
+in
+{
+  inherit
+    maxBound
+    minBound
+    abs
+    exp
+    odd
+    even
+    add
+    sub
+    mul
+    div
+    mod
+    bitShiftR
+    bitShiftL
+    bitOr
+    bitAnd
+    bitXor
+    toHex
+    fromHex
+    inRange
+    sum
+    ;
+}
diff --git a/users/sterni/nix/int/tests/default.nix b/users/sterni/nix/int/tests/default.nix
new file mode 100644
index 000000000000..8d2263b42117
--- /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/misc/default.nix b/users/sterni/nix/misc/default.nix
new file mode 100644
index 000000000000..1de9c973ec84
--- /dev/null
+++ b/users/sterni/nix/misc/default.nix
@@ -0,0 +1,18 @@
+{ ... }:
+
+let
+  /* Returns true if it is being evaluated using restrict-eval, false if not.
+     It's more robust than using `builtins.getEnv` since it isn't fooled by
+     `env -i`.
+
+     See https://github.com/NixOS/nix/issues/6579 for a description of the
+     behavior. Precise cause in the evaluator / store implementation is unclear.
+
+     Type: bool
+  */
+  inRestrictedEval = builtins.pathExists (toString ./guinea-pig + "/.");
+in
+
+{
+  inherit inRestrictedEval;
+}
diff --git a/users/sterni/nix/misc/guinea-pig b/users/sterni/nix/misc/guinea-pig
new file mode 120000
index 000000000000..73537e478e3f
--- /dev/null
+++ b/users/sterni/nix/misc/guinea-pig
@@ -0,0 +1 @@
+default.nix
\ No newline at end of file
diff --git a/users/sterni/nix/string/default.nix b/users/sterni/nix/string/default.nix
new file mode 100644
index 000000000000..381c8ddff748
--- /dev/null
+++ b/users/sterni/nix/string/default.nix
@@ -0,0 +1,121 @@
+{ 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 000000000000..e9015e95dca4
--- /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 000000000000..4a401873a1f2
--- /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 000000000000..4eb6f95ccd07
--- /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 000000000000..71c846c0421e
--- /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 000000000000..40783eab2421
--- /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 000000000000..1630ecb8f1da
--- /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.runCommand "${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/sterni/secrets/default.nix b/users/sterni/secrets/default.nix
new file mode 100644
index 000000000000..5550103c5a66
--- /dev/null
+++ b/users/sterni/secrets/default.nix
@@ -0,0 +1,3 @@
+{ depot, ... }:
+
+depot.ops.secrets.mkSecrets ./. (import ./secrets.nix)
diff --git a/users/sterni/secrets/minecraft-rcon.age b/users/sterni/secrets/minecraft-rcon.age
new file mode 100644
index 000000000000..7c896861b9d8
--- /dev/null
+++ b/users/sterni/secrets/minecraft-rcon.age
@@ -0,0 +1,9 @@
+age-encryption.org/v1
+-> ssh-ed25519 aXKGcg VELHhE9AlsAUspZj8M9zzOcjaml3/KSuNAae73TOOEk
+0vpPVz2TFMK2MLxHzMVO3a9QvnU9MfYcNO+JpMRRhN8
+-> ssh-ed25519 34g70A 28ldud+S2mz83kcIkEGv5XWWOdXUN/vetsqho7kiCh4
+/P+hJqj9r3KEi0VD15yg0MHyy0XgYUU5/zpMRrLaysM
+-> .-grease }}M
+ennsvHEhRup8I8R23GPWlILkCIMZmAuMT2F22SQPdjU
+--- e6u1rsLXltysnQqp3x73HfHLhqzTfkIV3mXaCtW1cxE
+!NiAO4P&Ԏ	FFRrz
\ No newline at end of file
diff --git a/users/sterni/secrets/secrets.nix b/users/sterni/secrets/secrets.nix
new file mode 100644
index 000000000000..d2f4860ff241
--- /dev/null
+++ b/users/sterni/secrets/secrets.nix
@@ -0,0 +1,15 @@
+let
+  nonremote = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJk+KvgvI2oJTppMASNUfMcMkA2G5ZNt+HnWDzaXKLlo"
+  ];
+
+  edwin = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB+OZ8f++cnvd4E2kFyn9jEoVpxi7LfjRvyQwzE8a5Ll"
+  ];
+in
+
+{
+  "warteraum-salt.age".publicKeys = nonremote ++ edwin;
+  "warteraum-tokens.age".publicKeys = nonremote ++ edwin;
+  "minecraft-rcon.age".publicKeys = nonremote ++ edwin;
+}
diff --git a/users/sterni/secrets/warteraum-salt.age b/users/sterni/secrets/warteraum-salt.age
new file mode 100644
index 000000000000..f932a881cd43
--- /dev/null
+++ b/users/sterni/secrets/warteraum-salt.age
Binary files differdiff --git a/users/sterni/secrets/warteraum-tokens.age b/users/sterni/secrets/warteraum-tokens.age
new file mode 100644
index 000000000000..37ab46981ecb
--- /dev/null
+++ b/users/sterni/secrets/warteraum-tokens.age
@@ -0,0 +1,10 @@
+age-encryption.org/v1
+-> ssh-ed25519 aXKGcg yHE1bla5BN1Kgows1tdeswamJQHfzGpv8fL3qZs04k0
+rR1O25EIQXctnyVsQCZO47bM44KFhmOZ7ePiecKrZ40
+-> ssh-ed25519 34g70A voVJDU9DIrT0z6X/mAi0tQqXthRZAyrzsPXOTIIzKUw
+yEiIaD9jblO44/RaoiPA0mjvRToNc4Ur9GcwfG9TSVo
+-> =UOH^-Z4-grease Do<;So
+l0F72v4UD8r5kbpNIT2i1IUT6ttXZhuPE91H2tucMc5TKRvGDvpdJNpQ+P+XmX2M
+661iYooyust5TGZsXJFHVYg
+--- To85A7ohH2Sjfy8js2+JzV0c86dmDO2JCH8TK7OtVtM
+Vq%n!M#`3;1wFCH'Y]-Q1m
\ No newline at end of file
diff --git a/users/tazjin/OWNERS b/users/tazjin/OWNERS
new file mode 100644
index 000000000000..ba1c06534863
--- /dev/null
+++ b/users/tazjin/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+tazjin
diff --git a/users/tazjin/aoc2019/default.nix b/users/tazjin/aoc2019/default.nix
new file mode 100644
index 000000000000..a1798f400174
--- /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 000000000000..d805c22ec870
--- /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 000000000000..6ecac1e2016c
--- /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 000000000000..b7dfdd245fb1
--- /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 000000000000..2805f3f4e9cd
--- /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 000000000000..cd89da7de412
--- /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 000000000000..a04f43d15197
--- /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 000000000000..5993bf3407e4
--- /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 000000000000..80ea4a226405
--- /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 000000000000..034a40a9558d
--- /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 000000000000..9bba322902b0
--- /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 000000000000..8179c79af2bd
--- /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 000000000000..251a85fede02
--- /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 000000000000..591a07fbf3a0
--- /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/aoc2022/day1.rs b/users/tazjin/aoc2022/day1.rs
new file mode 100644
index 000000000000..078eb25f037b
--- /dev/null
+++ b/users/tazjin/aoc2022/day1.rs
@@ -0,0 +1,27 @@
+// AoC 2022 - day 1.
+
+fn sum_elf(elf: &str) -> usize {
+    elf.lines()
+        .map(|s| s.parse::<usize>().expect("invalid input"))
+        .sum()
+}
+
+fn group_by_elf(input: &str) -> Vec<usize> {
+    input.rsplit("\n\n").map(sum_elf).collect()
+}
+
+fn top_elf(input: &str) -> usize {
+    group_by_elf(&input).into_iter().max().unwrap()
+}
+
+fn top_n_elves(n: usize, input: &str) -> usize {
+    let mut by_elf = group_by_elf(input);
+    by_elf.sort_by(|a, b| b.cmp(a)); // high->low
+    (by_elf[..n]).iter().sum()
+}
+
+fn main() {
+    let input = std::fs::read_to_string("input").expect("input should be in file named 'input'");
+    println!("top elf: {}", top_elf(&input));
+    println!("top 3 elves: {}", top_n_elves(3, &input));
+}
diff --git a/users/tazjin/avatar.jpeg b/users/tazjin/avatar.jpeg
new file mode 100644
index 000000000000..f6888e01c7dc
--- /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 000000000000..e7fa50d49bdd
--- /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 000000000000..85e9d29b3aae
--- /dev/null
+++ b/users/tazjin/blog/default.nix
@@ -0,0 +1,49 @@
+{ 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 = list post (import ./posts.nix);
+
+  rendered = pkgs.runCommand "tazjins-blog" { } ''
+    mkdir -p $out
+
+    ${lib.concatStringsSep "\n" (map (post:
+      "cp ${renderPost config post} $out/${post.key}.html"
+    ) posts)}
+  '';
+
+in
+{
+  inherit rendered config;
+
+  # Filter unlisted posts from the index
+  posts = filter includePost posts;
+
+  # 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 000000000000..f7f50d6f1a18
--- /dev/null
+++ b/users/tazjin/blog/posts.nix
@@ -0,0 +1,64 @@
+# 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";
+  }
+  {
+    key = "thoughts";
+    title = "Some thoughts";
+    date = 1665095948;
+    content = ./posts/thoughts.md;
+    listed = false;
+  }
+]
diff --git a/users/tazjin/blog/posts/best-tools.md b/users/tazjin/blog/posts/best-tools.md
new file mode 100644
index 000000000000..afe61767b18a
--- /dev/null
+++ b/users/tazjin/blog/posts/best-tools.md
@@ -0,0 +1,184 @@
+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 [Palm phone][palm-phone]. It's basically the smallest
+smartphone on the market at about the size of a credit card. I picked this phone
+because I am trying to not use a phone anymore, but some things simply require a
+smartphone.
+
+The Palm has terrible battery life but is otherwise great. I always have it in
+power saving mode and only turn on receiving calls when I'm expecting a call,.
+
+Previous phones I used and liked were:
+
+* [Unihertz Atom L][atom-l] - small-screen, rugged phone with headphone jack
+* Original [iPhone SE][se] - perfect-sized phone, unfortunately with iOS
+
+[palm-phone]: https://palm.com/pages/product
+[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 Coin Wallet][coin-wallet]. It's the slimmest wallet I
+could find that can deal with the volume of cards I (have to) carry around, as
+well as with cash.
+
+I've used Bellroy wallets for a long time, with the [Slim Sleeve][slim-sleeve]
+serving me in the days when I lived in no-cash countries.
+
+[coin-wallet]: https://bellroy.com/products/coin-wallet
+[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 000000000000..afb8dc889e53
--- /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 000000000000..420b57c0fde9
--- /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 000000000000..26526d11b5dc
--- /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/layers/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 000000000000..f8b326f2fb42
--- /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 000000000000..8968dc864590
--- /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 000000000000..0c43c5832d73
--- /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 000000000000..f411e3116046
--- /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/blog/posts/thoughts.md b/users/tazjin/blog/posts/thoughts.md
new file mode 100644
index 000000000000..7ce23f9c8779
--- /dev/null
+++ b/users/tazjin/blog/posts/thoughts.md
@@ -0,0 +1,142 @@
+<!--
+
+  This file contains a bunch of random thoughts I don't want to lose,
+  often resulting from conversation with other people, but that are
+  too far removed from what most people can relate to for me to just
+  publish them. Sometimes it's convenient to be able to share them,
+  though.
+
+  For that reason, if you stumble upon this file without me having
+  linked it to you intentionally, feel free to read it but keep the
+  sharing to a minimum (though do feel free to share the thoughts
+  themselves, of course).
+
+-->
+WARNING: This is not intended for a large audience. If you stumble
+upon this page by chance, please keep the sharing to a minimum.
+
+TIP: It's always work-in-progress. Things come and go. Or change. Who
+knows?
+
+---------
+
+### Three things
+
+*[mid/late 2020]*
+
+All things in the universe take the shape of one of approximately
+three things. If you had Hoogle for the entire universe, you'd
+probably find that one of them is `fmap`.
+
+There might be a few more, or a few less (or some may have been
+deprecated), but you get the idea. I guess [five][] would be a good
+number.
+
+[five]: https://principiadiscordia.com/book/23.php
+
+----------------------
+
+### Free energy principle
+
+*[mid/late 2020]*
+
+Karl Friston wrote:
+
+> The free-energy principle says that any self-organizing system that
+> is at equilibrium with its environment must minimize its free
+> energy.
+
+Or, somewhat paraphrased:
+
+> Any Markov blanket capable of modeling its environment aims to
+> reduce its level of surprise by either adapting its model, or
+> through other action.
+
+Seems reasonable to me.
+
+### More bizarre universe
+
+*[many years ago]*
+
+Douglas Adams wrote:
+
+> There is a theory which states that if ever anyone discovers exactly
+> what the Universe is for and why it is here, it will instantly
+> disappear and be replaced by something even more bizarre and
+> inexplicable. There is another theory which states that this has
+> already happened.
+
+### Alpha decay
+
+*[late 2022]*
+
+Finance people say:
+
+> Alpha Decay is commonly referred to as the loss of prediction power
+> of a trading strategy over time. As a consequence, the profitability
+> of a strategy tends to gradually decrease. Given enough time, the
+> strategy converges to having no superior predictive power and
+> returns when compared to a suitable benchmark.
+
+A market is a big optimiser. Any successful trading strategy adds
+friction in a place that the optimiser wants to remove.
+
+Alpha decay is unavoidable without changing and adapting the strategy.
+
+### Optimising universe
+
+*[late 2022]*
+
+*(thanks edef for helping me think through this one!)*
+
+Assume that the universe acts as a giant optimiser, and consider that
+the three things above are related and specialisations of more generic
+ideas:
+
+1. Every delineable entity in the universe (i.e. every *Markov
+   blanket*) attempts to reduce its level of surprise (the free energy
+   principle).
+
+2. The universe needs replacement (a more bizarre universe) if global
+   surprise drops to a minimum[^heat].
+
+3. Without improvement that outpaces the optimiser of the universe,
+   any strategy leading to (2) will get eroded by alpha decay long
+   before.
+
+4. We don't know if it is possible to outpace the optimiser from
+   within.
+
+On a personal note, it seems to me that achieving (2) is likely
+undesirable. It probably takes god[^god] a lot of resources to create
+an ever more complex universe and this process might be much less
+enjoyable than "running" (for lack of a better word) a universe. Under
+this assumption, a universe that achieves (2) faster than others might
+be a failure, and on a higher level conditions leading to its creation
+might be subject to another optimiser.
+
+Or it could be the other way around, but this seems more likely to me
+personally.
+
+### Superintelligence
+
+*[late 2022]*
+
+Under the previous assumption, achieving superintelligence is likely a
+bad idea for anyone feeling some kind of attachment to *this*
+universe.
+
+Or it might be the exact opposite, but I don't think so.
+
+-------------------------------
+
+[^heat]: Note that this is consistent with the heat death of the
+    universe.
+
+[^god]: I'm using the word "god" as the best English approximation of
+    a concept that different religions and philosophies all attempt to
+    approach. I think that for many cognitive purposes, an
+    anthropomorphised idea (as in the abrahamic religions) is useful,
+    but ideas from some Eastern religions or modern philosophers like
+    Bach or Watts are likely more aligned with the "nature of things"
+    as such.
diff --git a/users/tazjin/default.nix b/users/tazjin/default.nix
new file mode 100644
index 000000000000..1b68b7127a72
--- /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 000000000000..6ff6cc06e249
--- /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.runCommand "${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 000000000000..8ea1d694c9a1
--- /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 000000000000..326a129d2105
--- /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 000000000000..43db5834a0ca
--- /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 000000000000..415af30fd41d
--- /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 000000000000..de2c99ae6007
--- /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 000000000000..9b783a9c857c
--- /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 000000000000..2aa1141b6ec2
--- /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 000000000000..2af3b9433a6d
--- /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 000000000000..a490774e635f
--- /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 000000000000..7b666905f847
--- /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 000000000000..5c667333962e
--- /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 000000000000..1feb9faf8186
--- /dev/null
+++ b/users/tazjin/emacs/config/bindings.el
@@ -0,0 +1,111 @@
+;; 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
+
+;; Get rid of the annoying `save-some-buffers' shortcut which I
+;; *NEVER* use intentionally.
+(unbind-key (kbd "C-x s") 'global-map)
+
+;; German keyboard layout with Y and Z in the correct place.
+
+(quail-define-package
+ "german-qwerty" "German" "DE@" t
+ "German (Deutsch) input method with QWERTY keys"
+ nil t t t t nil nil nil nil nil t)
+
+;; 1!  2"  3§  4$  5%  6&  7/  8(  9)  0=  ß?  [{  ]}
+;;  qQ  wW  eE  rR  tT  yY  uU  iI  oO  pP  üÜ  +*
+;;   aA  sS  dD  fF  gG  hH  jJ  kK  lL  öÖ  äÄ  #^
+;;    zZ  xX  cC  vV  bB  nN  mM  ,;  .:  -_
+
+(quail-define-rules
+ ("-" ?ß)
+ ("=" ?\[)
+ ("`" ?\])
+ ("[" ?ü)
+ ("]" ?+)
+ (";" ?ö)
+ ("'" ?ä)
+ ("\\" ?#)
+ ("/" ?-)
+
+ ("@" ?\")
+ ("#" ?§)
+ ("^" ?&)
+ ("&" ?/)
+ ("*" ?\()
+ ("(" ?\))
+ (")" ?=)
+ ("_" ??)
+ ("+" ?{)
+ ("~" ?})
+ ("{" ?Ü)
+ ("}" ?*)
+ (":" ?Ö)
+ ("\"" ?Ä)
+ ("|" ?^)
+ ("<" ?\;)
+ (">" ?:)
+ ("?" ?_))
+
+(provide 'bindings)
diff --git a/users/tazjin/emacs/config/custom.el b/users/tazjin/emacs/config/custom.el
new file mode 100644
index 000000000000..91eaf69ae59b
--- /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 000000000000..83336f74c200
--- /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 8)
+     ("eDP" 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 000000000000..0b23c5a2d1bc
--- /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 000000000000..23cb47d02a3b
--- /dev/null
+++ b/users/tazjin/emacs/config/functions.el
@@ -0,0 +1,369 @@
+(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 screenshot-select (filename)
+  "Take a screenshot based on a mouse-selection and save it to
+  ~/screenshots."
+  (interactive "sScreenshot filename: ")
+  (let* ((path (f-join "~/screenshots"
+                       (format "%s-%d.png"
+                               (if (string-empty-p filename) "shot" filename)
+                               (time-convert nil 'integer)))))
+    (shell-command (format "maim --select %s" path))
+    (message "Wrote screenshot to %s" path)))
+
+(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 find-cargo-project (dir)
+  "Attempt to find the current project in `project-find-functions'
+by looking for a `Cargo.toml' file."
+  (unless (equal "/" dir)
+    (if (f-exists-p (f-join dir "Cargo.toml"))
+        (cons 'transient dir)
+      (find-cargo-project (f-parent dir)))))
+
+(add-to-list 'project-find-functions #'find-cargo-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))
+
+(defun toggle-nix-test-and-exp ()
+  "Switch between the .nix and .exp file in a Tvix/Nix test."
+  (interactive)
+  (let* ((file (buffer-file-name))
+         (other (if (s-suffix? ".nix" file)
+                    (s-replace-regexp ".nix$" ".exp" file)
+                  (if (s-suffix? ".exp" file)
+                      (s-replace-regexp ".exp$" ".nix" file)
+                    (error "Not a .nix/.exp file!")))))
+    (find-file other)))
+
+(provide 'functions)
diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el
new file mode 100644
index 000000000000..7eb1a19a8e57
--- /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-at-point)
+
+(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 000000000000..2e5c2f94990b
--- /dev/null
+++ b/users/tazjin/emacs/config/look-and-feel.el
@@ -0,0 +1,122 @@
+;;; -*- 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 (format "JetBrains Mono Medium-%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 000000000000..7fbece1b102a
--- /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 000000000000..69fb523d0d91
--- /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 000000000000..8b15b6cda183
--- /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 000000000000..23a0f2bed12f
--- /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.emacsUnstable }:
+  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-at-point
+      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.runCommand "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 000000000000..548206b0b297
--- /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 000000000000..7427a6b11c42
--- /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 000000000000..310133abeebb
--- /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 000000000000..5acd67d3bea7
--- /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 000000000000..9a39591eab1d
--- /dev/null
+++ b/users/tazjin/finito/default.nix
@@ -0,0 +1,9 @@
+{ depot, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+
+  # Got broken by a rustc update (?)
+  # https://buildkite.com/tvl/depot/builds/17910#01841493-dc42-44f8-b904-32bf3d835485
+  meta.ci.skip = true;
+}
diff --git a/users/tazjin/finito/finito-core/Cargo.toml b/users/tazjin/finito/finito-core/Cargo.toml
new file mode 100644
index 000000000000..1d7bdb8b01fe
--- /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 000000000000..aaec03a77b42
--- /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 000000000000..32c0a5a7c4ef
--- /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 000000000000..441ab0e3d2cf
--- /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 000000000000..dd8d1d000304
--- /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 000000000000..9b56f9d35abe
--- /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 000000000000..18ace393b8d9
--- /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 000000000000..ed33775cd70e
--- /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 000000000000..ea63cc9dfdfd
--- /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 000000000000..dd270c38759f
--- /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/generator-example/.gitignore b/users/tazjin/generator-example/.gitignore
new file mode 100644
index 000000000000..ea8c4bf7f35f
--- /dev/null
+++ b/users/tazjin/generator-example/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/users/tazjin/generator-example/Cargo.lock b/users/tazjin/generator-example/Cargo.lock
new file mode 100644
index 000000000000..a6f25ee39429
--- /dev/null
+++ b/users/tazjin/generator-example/Cargo.lock
@@ -0,0 +1,124 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "genawaiter"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
+dependencies = [
+ "genawaiter-macro",
+ "genawaiter-proc-macro",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "genawaiter-macro"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
+
+[[package]]
+name = "genawaiter-proc-macro"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "generator-example"
+version = "0.1.0"
+dependencies = [
+ "genawaiter",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "syn-mid",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn-mid"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa8e7560a164edb1621a55d18a0c59abf49d360f47aa7b821061dd7eea7fac9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
diff --git a/users/tazjin/generator-example/Cargo.toml b/users/tazjin/generator-example/Cargo.toml
new file mode 100644
index 000000000000..faf313973f5e
--- /dev/null
+++ b/users/tazjin/generator-example/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "generator-example"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+genawaiter = "0.99.1"
diff --git a/users/tazjin/generator-example/README.md b/users/tazjin/generator-example/README.md
new file mode 100644
index 000000000000..0bec13ee9a6b
--- /dev/null
+++ b/users/tazjin/generator-example/README.md
@@ -0,0 +1,11 @@
+generator-example
+=================
+
+This is an experiment with the [`genawaiter`][] crate, to see if it
+could be suitable for dealing with the execution flattening problem in
+Tvix.
+
+It constructs a dummy example that is similar to some of the problems
+we have in Tvix that require generator-like thunk forcing.
+
+[`genawaiter`]: https://docs.rs/genawaiter/latest/genawaiter/index.html
diff --git a/users/tazjin/generator-example/src/main.rs b/users/tazjin/generator-example/src/main.rs
new file mode 100644
index 000000000000..4aa931caf853
--- /dev/null
+++ b/users/tazjin/generator-example/src/main.rs
@@ -0,0 +1,115 @@
+use genawaiter::rc::{Co, Gen};
+use std::cell::RefCell;
+use std::future::Future;
+use std::pin::Pin;
+use std::rc::Rc;
+
+#[derive(Debug)]
+enum ValueRepr {
+    Int(i64),
+    Thunk((i64, i64)),
+}
+
+#[derive(Clone, Debug)]
+struct Value(Rc<RefCell<ValueRepr>>);
+
+impl Value {
+    fn force(&self) {
+        let mut inner = self.0.borrow_mut();
+        match *inner {
+            ValueRepr::Int(_) => return,
+            ValueRepr::Thunk((a, b)) => {
+                *inner = ValueRepr::Int(a + b);
+            }
+        }
+    }
+
+    fn is_forced(&self) -> bool {
+        matches!(*self.0.borrow(), ValueRepr::Int(_))
+    }
+
+    fn int(&self) -> i64 {
+        match *self.0.borrow() {
+            ValueRepr::Int(i) => i,
+            ValueRepr::Thunk(_) => panic!("unforced thunk!"),
+        }
+    }
+}
+
+impl From<i64> for Value {
+    fn from(value: i64) -> Self {
+        Value(Rc::new(RefCell::new(ValueRepr::Int(value))))
+    }
+}
+
+impl From<(i64, i64)> for Value {
+    fn from(value: (i64, i64)) -> Self {
+        Value(Rc::new(RefCell::new(ValueRepr::Thunk(value))))
+    }
+}
+
+async fn list_maker(values: Vec<Value>, co: Co<Value>) -> Vec<i64> {
+    let mut output: Vec<i64> = vec![];
+
+    for value in values {
+        if !value.is_forced() {
+            co.yield_(value.clone()).await;
+        }
+
+        output.push(value.int());
+    }
+
+    output
+}
+
+async fn list_reverser(values: Vec<Value>, co: Co<Value>) -> Vec<i64> {
+    let mut output = list_maker(values, co).await;
+    output.reverse();
+    output
+}
+
+struct Frame {
+    gen: Gen<Value, (), Pin<Box<dyn Future<Output = Vec<i64>>>>>,
+}
+
+fn pin_future(
+    f: impl Future<Output = Vec<i64>> + 'static,
+) -> Pin<Box<dyn Future<Output = Vec<i64>>>> {
+    Box::pin(f)
+}
+
+fn main() {
+    let mut frames: Vec<Frame> = vec![];
+
+    let values: Vec<Value> = vec![
+        42.into(),
+        (12, 54).into(),
+        4.into(),
+        (40, 2).into(),
+        2.into(),
+    ];
+    let second = values.clone();
+
+    frames.push(Frame {
+        gen: Gen::new(|co| pin_future(list_maker(values, co))),
+    });
+
+    frames.push(Frame {
+        gen: Gen::new(|co| pin_future(list_reverser(second, co))),
+    });
+
+    for (idx, mut frame) in frames.into_iter().enumerate() {
+        loop {
+            match frame.gen.resume() {
+                genawaiter::GeneratorState::Yielded(val) => {
+                    println!("yielded {:?} in frame {}", val, idx);
+                    val.force();
+                }
+                genawaiter::GeneratorState::Complete(list) => {
+                    println!("result {}: {:?}", idx, list);
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/users/tazjin/gruber-darker.qss b/users/tazjin/gruber-darker.qss
new file mode 100644
index 000000000000..16f4c2f32991
--- /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 000000000000..daeb41c9aabb
--- /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 000000000000..5788f2aa8701
--- /dev/null
+++ b/users/tazjin/home/shared.nix
@@ -0,0 +1,94 @@
+# Shared home configuration for all machines.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [ (depot.third_party.sources.impermanence + "/home-manager.nix") ];
+
+  home.persistence."/persist/tazjin/home" = {
+    allowOther = true;
+
+    directories = [
+      ".cargo"
+      ".config/audacity"
+      ".config/chromium"
+      ".config/google-chrome"
+      ".config/quassel-irc.org"
+      ".config/syncthing"
+      ".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"
+      ".thunderbird"
+      "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;
+    inactiveInterval = 10; # minutes
+    lockCmd = "${depot.users.tazjin.screenLock}/bin/tazjin-screen-lock";
+  };
+
+  services.picom = {
+    enable = true;
+    vSync = true;
+    backend = "glx";
+  };
+
+  services.syncthing.enable = true;
+
+  # 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;
+
+  # Previous default version, see https://github.com/nix-community/home-manager/blob/master/docs/release-notes/rl-2211.adoc
+  home.stateVersion = "18.09";
+}
diff --git a/users/tazjin/home/tverskoy.nix b/users/tazjin/home/tverskoy.nix
new file mode 100644
index 000000000000..d72734920ea6
--- /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 000000000000..6bac67eb1c4d
--- /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 000000000000..15f4d787c0b5
--- /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 runCommand;
+
+  # 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 (runCommand "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
+runCommand "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 000000000000..9e43516e5358
--- /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 000000000000..09bc36341401
--- /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 000000000000..2f17135066e8
--- /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 000000000000..5a22d9eb7b45
--- /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 000000000000..f99c9085340b
--- /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 000000000000..2d8569a6ca21
--- /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 000000000000..101f0f633aef
--- /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 000000000000..a2b90b3e21d5
--- /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 000000000000..0e9f74556682
--- /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 000000000000..27e588e1a145
--- /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 000000000000..b2dea98b4fb4
--- /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 000000000000..99b49373b5b4
--- /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 000000000000..41dbdb6be1cf
--- /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 000000000000..c42a4ce1bc0f
--- /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 000000000000..1440761859dd
--- /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 000000000000..f15fcb69e886
--- /dev/null
+++ b/users/tazjin/keys/default.nix
@@ -0,0 +1,10 @@
+# My SSH public keys
+{ ... }:
+
+let withAll = keys: keys // { all = builtins.attrValues keys; };
+in withAll {
+  tverskoy = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBAWvA3RpXpMAqruUbB+eVgvvHCzhs5R9khFRza3YSLeFiIqOxVVgyhzW/BnCSD9t/5JrqRdJIGQLnkQU9m4REhUAAAAEc3NoOg== tazjin@tverskoy";
+  tverskoy_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1fGWz/gsq+ZeZXjvUrV+pBlanw1c3zJ9kLTax9FWQy tazjin@tverskoy";
+  zamalek_sk = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOAw3OaPAjnC6hArGYEmBoXhPf7aZdRGlDZcSqm6gbB8AAAABHNzaDo= tazjin@zamalek";
+  zamalek_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBRXeb8EuecLHP0bW4zuebXp4KRnXgJTZfeVWXQ1n1R tazjin@zamalek";
+}
diff --git a/users/tazjin/kinesis/README.md b/users/tazjin/kinesis/README.md
new file mode 100644
index 000000000000..7cd95a5e5f3c
--- /dev/null
+++ b/users/tazjin/kinesis/README.md
@@ -0,0 +1,10 @@
+Kinesis configuration
+=====================
+
+This folder backs up the configuration for my Kinesis keyboards.
+Configuration is not mutually compatible between the Advantage 2 and
+the Advantage 360, so they are stored in different folders and
+(mostly) programmed on-board.
+
+I keep these around in case I get a new keyboard and want to bootstrap
+it to behave the same way as the previous one.
diff --git a/users/tazjin/kinesis/advantage2/qwerty.txt b/users/tazjin/kinesis/advantage2/qwerty.txt
new file mode 100755
index 000000000000..624e809c2282
--- /dev/null
+++ b/users/tazjin/kinesis/advantage2/qwerty.txt
@@ -0,0 +1,6 @@
+[caps]>[rwin]

+[lctrl]>[lalt]

+[delete]>[lctrl]

+[rctrl]>[rwin]

+{pup}>{-rwin}{b}{+rwin}

+{pdown}>{-rwin}{f}{+rwin}

diff --git a/users/tazjin/nisp/transform.el b/users/tazjin/nisp/transform.el
new file mode 100644
index 000000000000..89b2bb104d27
--- /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 000000000000..4da795a4362b
--- /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 000000000000..212d3ad270f4
--- /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 000000000000..662f2a36acac
--- /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 000000000000..2f34110eb528
--- /dev/null
+++ b/users/tazjin/nixos/camden/default.nix
@@ -0,0 +1,345 @@
+# 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;
+    };
+
+    tmp.cleanOnBoot = 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.settings = {
+    max-jobs = lib.mkDefault 4;
+    trusted-users = [ "root" "tazjin" ];
+    substituters = [
+      "https://tazjin.cachix.org"
+    ];
+
+    trusted-public-keys = [
+      "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";
+      postRun = "systemctl reload nginx";
+
+      extraDomainNames = [
+        "cs.tazj.in"
+        "git.tazj.in"
+        "www.tazj.in"
+
+        # Local domains (for this machine only)
+        "camden.tazj.in"
+      ];
+    };
+
+    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 000000000000..b9cae51d7f69
--- /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 000000000000..54d6def0239a
--- /dev/null
+++ b/users/tazjin/nixos/frog/default.nix
@@ -0,0 +1,286 @@
+{ 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 = {
+    tmp.useTmpfs = 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.settings = {
+    max-jobs = 48;
+    substituters = [ "ssh://nix-ssh@whitby.tvl.fyi" ];
+    trusted-public-keys = [ "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
+      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/chromium.nix b/users/tazjin/nixos/modules/chromium.nix
new file mode 100644
index 000000000000..22f1c8d362fe
--- /dev/null
+++ b/users/tazjin/nixos/modules/chromium.nix
@@ -0,0 +1,30 @@
+# Configure the Chromium browser with various useful things.
+{ pkgs, ... }:
+
+{
+  environment.systemPackages = [
+    (pkgs.chromium.override {
+      enableWideVine = true; # DRM support (for Кинопоиск)
+    })
+  ];
+
+  programs.chromium = {
+    enable = true;
+    homepageLocation = "about:blank";
+
+    extensions = [
+      "dbepggeogbaibhgnhhndojpepiihcmeb" # Vimium
+      "cjpalhdlnbpafiamejdnhcphjbkeiagm" # uBlock Origin
+      "mohaicophfnifehkkkdbcejkflmgfkof" # nitter redirect
+      "lhdifindchogekmjooeiolmjdlheilae" # Huruf
+    ];
+
+    extraOpts = {
+      SpellcheckEnabled = true;
+      SpellcheckLanguage = [
+        "ru"
+        "en-GB"
+      ];
+    };
+  };
+}
diff --git a/users/tazjin/nixos/modules/default.nix b/users/tazjin/nixos/modules/default.nix
new file mode 100644
index 000000000000..d747e8e1319a
--- /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 000000000000..c78463386c46
--- /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 000000000000..3b4461056f24
--- /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 000000000000..2ff61d499a2e
--- /dev/null
+++ b/users/tazjin/nixos/modules/hidpi.nix
@@ -0,0 +1,19 @@
+# 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";
+
+  # TODO(tazjin): this option has been removed and needs to be replaced
+  # by manual configuration: https://github.com/NixOS/nixpkgs/issues/222805
+  # 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 000000000000..bda8f7a44014
--- /dev/null
+++ b/users/tazjin/nixos/modules/home-config.nix
@@ -0,0 +1,19 @@
+# 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 = "$2b$05$1eBPdoIgan/C/L8JFqIHBuVscQyTKw1L/4VBlzlLvLBEf6CXS3EW6";
+  };
+
+  nix.settings.trusted-users = [ "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 000000000000..e0d67dc25989
--- /dev/null
+++ b/users/tazjin/nixos/modules/laptop.nix
@@ -0,0 +1,15 @@
+# Configuration specifically for laptops that move around.
+{ ... }:
+
+{
+  time.timeZone = "Europe/Moscow";
+
+  # Automatically detect location for redshift & so on ...
+  services.geoclue2.enable = true;
+  location.provider = "geoclue2";
+
+  # 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 000000000000..b864d13a8d70
--- /dev/null
+++ b/users/tazjin/nixos/modules/persistence.nix
@@ -0,0 +1,26 @@
+# Configuration for persistent (non-home) data.
+{ config, depot, pkgs, lib, ... }:
+
+{
+  imports = [
+    (depot.third_party.sources.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 = lib.optional (builtins.isNull config.networking.hostId) [
+      "/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 000000000000..0105d9f3a2a1
--- /dev/null
+++ b/users/tazjin/nixos/modules/physical.nix
@@ -0,0 +1,93 @@
+# Default configuration settings for physical machines that I use.
+{ lib, 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
+      third_party.josh
+    ]) ++
+
+    # programs from nixpkgs
+    (with pkgs; [
+      amber
+      bat
+      curl
+      ddcutil
+      direnv
+      dnsutils
+      electrum
+      emacsUnstable # emacsclient
+      exa
+      fd
+      file
+      gdb
+      gh
+      git
+      gnupg
+      gtk3 # for gtk-launch
+      htop
+      hyperfine
+      iftop
+      imagemagick
+      jq
+      lieer
+      maim
+      man-pages
+      moreutils
+      mosh
+      msmtp
+      mullvad-vpn
+      networkmanagerapplet
+      nix-prefetch-github
+      nmap
+      notmuch
+      openssh
+      openssl
+      pass-otp
+      pavucontrol
+      pinentry
+      pinentry-emacs
+      pulseaudio # for pactl
+      pwgen
+      quasselClient
+      rink
+      ripgrep
+      rust-analyzer
+      rustup
+      screen
+      thunderbird
+      tig
+      tokei
+      tree
+      unzip
+      vlc
+      volumeicon
+      whois
+      xclip
+      xsecurelock
+      zoxide
+    ]);
+
+  # Run services & configure programs for all machines.
+  services = {
+    mullvad-vpn.enable = true;
+    fwupd.enable = true;
+  };
+
+  # Disable the broken NetworkManager-wait-online.service
+  systemd.services.NetworkManager-wait-online.enable = lib.mkForce false;
+
+  programs = {
+    fish.enable = true;
+    mosh.enable = true;
+    ssh.startAgent = true;
+  };
+}
diff --git a/users/tazjin/nixos/modules/predlozhnik.nix b/users/tazjin/nixos/modules/predlozhnik.nix
new file mode 100644
index 000000000000..db20963df1f2
--- /dev/null
+++ b/users/tazjin/nixos/modules/predlozhnik.nix
@@ -0,0 +1,10 @@
+# Host predlozhnik.ru, serving //users/tazjin/predlozhnik
+{ depot, ... }:
+
+{
+  services.nginx.virtualHosts."predlozhnik.ru" = {
+    root = depot.corp.russian.predlozhnik;
+    enableACME = true;
+    forceSSL = true;
+  };
+}
diff --git a/users/tazjin/nixos/modules/tgsa.nix b/users/tazjin/nixos/modules/tgsa.nix
new file mode 100644
index 000000000000..9953ab03acae
--- /dev/null
+++ b/users/tazjin/nixos/modules/tgsa.nix
@@ -0,0 +1,25 @@
+{ config, depot, lib, pkgs, ... }:
+
+{
+  systemd.services.tgsa = {
+    description = "telegram -> SA bbcode thing";
+    wantedBy = [ "multi-user.target" ];
+
+    serviceConfig = {
+      EnvironmentFile = "/root/tgsa-env"; # TODO(tazjin): automate this
+      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/polyanka/default.nix b/users/tazjin/nixos/polyanka/default.nix
new file mode 100644
index 000000000000..c7f41b95aa0c
--- /dev/null
+++ b/users/tazjin/nixos/polyanka/default.nix
@@ -0,0 +1,135 @@
+# 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")
+    (usermod "predlozhnik.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";
+  };
+
+  programs.fish.enable = true;
+
+  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"
+    ];
+  };
+
+  # Automatically collect garbage from the Nix store.
+  services.depot.automatic-gc = {
+    enable = true;
+    interval = "daily";
+    diskThreshold = 2; # GiB
+    maxFreed = 8; # GiB
+    preserveGenerations = "14d";
+  };
+
+  # 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.tailscale.enable = true;
+  boot.kernel.sysctl = {
+    "net.ipv4.ip_forward" = 1;
+    "net.ipv6.conf.all.forwarding" = 1;
+  };
+
+  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 000000000000..513f65966fce
--- /dev/null
+++ b/users/tazjin/nixos/tverskoy/default.nix
@@ -0,0 +1,170 @@
+# 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 "chromium.nix")
+    (usermod "desktop.nix")
+    (usermod "fonts.nix")
+    (usermod "home-config.nix")
+    (usermod "laptop.nix")
+    (usermod "persistence.nix")
+    (usermod "physical.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.zfsUnstable.latestCompatibleLinuxPackages;
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+    zfs.enableUnstable = true;
+  };
+
+  virtualisation.virtualbox.host.enable = true;
+  virtualisation.docker.enable = true;
+  users.users.tazjin.extraGroups = [ "docker" "vboxusers" ];
+
+  fileSystems = {
+    "/" = {
+      device = "zpool/ephemeral/root";
+      fsType = "zfs";
+    };
+
+    "/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 = {
+    tailscale.enable = true;
+    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";
+    };
+  };
+
+  # systemd-oomd seems to have been enabled by default around ~
+  # December 2022, and it's really into killing my X session as soon
+  # as I do anything stressful to the machine
+  systemd.services.systemd-oomd.enable = lib.mkForce false;
+
+  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 000000000000..a340e8a3e897
--- /dev/null
+++ b/users/tazjin/nixos/zamalek/default.nix
@@ -0,0 +1,88 @@
+# 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 "chromium.nix")
+    (usermod "desktop.nix")
+    (usermod "fonts.nix")
+    (usermod "hidpi.nix")
+    (usermod "home-config.nix")
+    (usermod "laptop.nix")
+    (usermod "persistence.nix")
+    (usermod "physical.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;
+
+    extraHosts = ''
+      10.101.240.1 wifi.silja.fi
+    '';
+
+    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.xserver.libinput.touchpad.tapping = false;
+  services.avahi.enable = true;
+  services.tailscale.enable = true;
+  powerManagement.powertop.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 000000000000..e9573ae3f2e1
--- /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 000000000000..2775d0b3fbbb
--- /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 000000000000..4b036754384f
--- /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 000000000000..ce0c98c2cabb
--- /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 000000000000..363d75352e62
--- /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 000000000000..7f435fe5b539
--- /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 000000000000..d3aa61337554
--- /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 000000000000..5644dc3900e3
--- /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 000000000000..b0fa6c9a0ef8
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..e1b6c83b99cc
--- /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 000000000000..526564b88268
--- /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 000000000000..ec8d996704b2
--- /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 000000000000..56404a0c5a20
--- /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 000000000000..ca78261399e1
--- /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 000000000000..2d1f6c84c401
--- /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 000000000000..01df14ac57d5
--- /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 000000000000..24bb934ee503
--- /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 000000000000..7fee0928c575
--- /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 000000000000..96115ec2cbfc
--- /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 000000000000..8cfb04a42417
--- /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 000000000000..842a667e1bcc
--- /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 000000000000..ed46003768c0
--- /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 000000000000..d5947eb9421a
--- /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 000000000000..1a38620fe9cc
--- /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 000000000000..108b3507ddd1
--- /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 000000000000..ac5dde3cb32f
--- /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 000000000000..7f004b7d14ca
--- /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 000000000000..b2d4c9d34799
--- /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 000000000000..998185d26177
--- /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 000000000000..e25524b4e230
--- /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 000000000000..87f1342da995
--- /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 000000000000..7e14f13e29d9
--- /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 000000000000..34eccb98b02a
--- /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 000000000000..fcc710ad933f
--- /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 000000000000..b4866b1642bb
--- /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 000000000000..384db2a6e0af
--- /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 000000000000..99326bd8bf4e
--- /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 000000000000..c613cefd7ec4
--- /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 000000000000..a22c762f7e13
--- /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 000000000000..29e65519ba35
--- /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 000000000000..d8107726e067
--- /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 000000000000..b66af6ba85d3
--- /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 000000000000..1d2692d09cc1
--- /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 000000000000..e50ac32be452
--- /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 000000000000..39af1d73c4c9
--- /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 000000000000..1b91e9db94ac
--- /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 000000000000..d197ad11383f
--- /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 000000000000..31752d9e2f5e
--- /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 000000000000..5f335c0e8b29
--- /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 000000000000..d563807943ba
--- /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 000000000000..f826c8658803
--- /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 000000000000..dd6fb5e4bf4d
--- /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 000000000000..7af90b3f0bee
--- /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 000000000000..df99c69198f5
--- /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 000000000000..fc5cd34fdf4f
--- /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 000000000000..89584f19d720
--- /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 000000000000..988031f763cf
--- /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 000000000000..1da1a24b2c5f
--- /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 000000000000..b34bf6835389
--- /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 000000000000..117f17824ac6
--- /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 000000000000..8a106f96917d
--- /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 000000000000..bc7d6cb878f8
--- /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 000000000000..4170efadf8fe
--- /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 000000000000..30ffebc79cf7
--- /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 000000000000..ee61ae01a106
--- /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 000000000000..314b56d6d380
--- /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 000000000000..391663d51b18
--- /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 000000000000..3285775bbec6
--- /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 000000000000..c502d2a1718a
--- /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 000000000000..2fc6f4fee978
--- /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 000000000000..2d82b3320a90
--- /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 000000000000..5794b42d1577
--- /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 000000000000..3d12973aa089
--- /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 000000000000..41d4aa34f47e
--- /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 000000000000..77d09b47261c
--- /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 000000000000..28f1addeaa04
--- /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 000000000000..784e5bddde5e
--- /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 000000000000..0c719dcfec6f
--- /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 000000000000..29e65519ba35
--- /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 000000000000..51d11135f37f
--- /dev/null
+++ b/users/tazjin/tgsa/Cargo.lock
@@ -0,0 +1,1339 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+
+[[package]]
+name = "ascii"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
+
+[[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 = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+
+[[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.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+dependencies = [
+ "iana-time-zone",
+ "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 = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "crimp"
+version = "4087.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ead2c83f7d1f9b8e5a6f7a25985d0d1759ccd2cd72abb1eee2db65d05e12b39"
+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 0.8.0",
+ "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.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.56+curl-7.83.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6093e169dd4de29e468fa649fbae11cdcd5551c81fe5bf1b0677adad7ef3d26f"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[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.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[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.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+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.134"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "lock_api"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+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 = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
+dependencies = [
+ "log",
+ "phf 0.10.1",
+ "phf_codegen 0.10.0",
+ "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.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 = "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.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 = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[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.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+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 = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[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"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_shared 0.10.0",
+]
+
+[[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_codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.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.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
+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.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+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.4",
+]
+
+[[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.4",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.7",
+]
+
+[[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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+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",
+ "chrono",
+ "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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[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.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
+
+[[package]]
+name = "scraper"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5684396b456f3eb69ceeb34d1b5cb1a2f6acf7ca4452131efa3ba0ee2c2d0a70"
+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 0.8.0",
+ "phf_codegen 0.8.0",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+ "thin-slice",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
+
+[[package]]
+name = "serde"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+dependencies = [
+ "itoa 1.0.3",
+ "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.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+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.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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",
+ "serde",
+ "serde_json",
+ "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.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
+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.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 = "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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "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.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[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.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"
diff --git a/users/tazjin/tgsa/Cargo.toml b/users/tazjin/tgsa/Cargo.toml
new file mode 100644
index 000000000000..0b1529805864
--- /dev/null
+++ b/users/tazjin/tgsa/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "tgsa"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0"
+crimp = "4087.0"
+rouille = { version = "3.5", default-features = false }
+url = "2.3"
+scraper = "0.13"
+ego-tree = "0.6" # in tandem with 'scraper'
+serde = "1.0"
+serde_json = "1.0"
diff --git a/users/tazjin/tgsa/default.nix b/users/tazjin/tgsa/default.nix
new file mode 100644
index 000000000000..e413c99116fb
--- /dev/null
+++ b/users/tazjin/tgsa/default.nix
@@ -0,0 +1,14 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = depot.nix.sparseTree ./. [
+    ./Cargo.lock
+    ./Cargo.toml
+    ./src
+  ];
+
+  buildInputs = with pkgs; [
+    pkg-config
+    openssl
+  ];
+}
diff --git a/users/tazjin/tgsa/src/main.rs b/users/tazjin/tgsa/src/main.rs
new file mode 100644
index 000000000000..1c7ee408abe4
--- /dev/null
+++ b/users/tazjin/tgsa/src/main.rs
@@ -0,0 +1,442 @@
+use anyhow::{anyhow, Context, Result};
+use scraper::{Html, Selector};
+use serde_json::Value;
+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,
+    translated: bool,
+}
+
+impl TgLink {
+    fn human_friendly_url(&self) -> String {
+        format!("t.me/{}/{}", self.username, self.message_id)
+    }
+
+    fn to_url(&self, embed: bool) -> String {
+        format!(
+            "https://t.me/{}/{}{}",
+            self.username,
+            self.message_id,
+            if embed { "?embed=1" } else { "" }
+        )
+    }
+
+    fn parse(url: &str, translated: bool) -> 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()?,
+            translated,
+        })
+    }
+}
+
+fn fetch_post(link: &TgLink, embed: bool) -> Result<String> {
+    println!("fetching {}#{}", link.username, link.message_id);
+    let response = crimp::Request::get(&link.to_url(embed))
+        .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)
+}
+
+fn fetch_translation(message: &str) -> Result<String> {
+    let request = serde_json::json!({
+        "model": "gpt-3.5-turbo",
+        "messages": [
+            {"role": "user", "content": "Please translate the following message from a Telegram channel into English. If the post is already partially in English, please leave those bits intact as they are. Please respond only with the translation."},
+            {"role": "user", "content": message}
+        ]
+    });
+
+    let response: Value = crimp::Request::post("https://api.openai.com/v1/chat/completions")
+        .bearer_auth(&std::env::var("OPENAPI_KEY").context("no openapi key set")?)?
+        .json(&request)?
+        .send()
+        .context("failed to fetch translation from openai")?
+        .as_json::<Value>()?
+        .error_for_status(|resp| {
+            anyhow!(
+                "translation request failed: {} ({})",
+                resp.body,
+                resp.status
+            )
+        })?
+        .body;
+
+    // we want choices[0].message.content, and inshallah it's the right thing.
+    let translation = response
+        .get("choices")
+        .ok_or_else(|| anyhow!("missing 'choices' key"))?
+        .get(0)
+        .ok_or_else(|| anyhow!("empty 'choices' or something"))?
+        .get("message")
+        .ok_or_else(|| anyhow!("missing 'message' key"))?
+        .get("content")
+        .ok_or_else(|| anyhow!("missing 'content' key"))?
+        .as_str()
+        .ok_or_else(|| anyhow!("'content' was not a string"))?;
+
+    Ok(translation.to_string())
+}
+
+// in some cases, posts can not be embedded, but telegram still
+// includes their content in metadata tags for content previews.
+//
+// we skip images in this case, as they are scaled down to thumbnail
+// size and not useful.
+fn fetch_fallback(link: &TgLink) -> Result<Option<String>> {
+    let post = fetch_post(link, false)?;
+    let doc = Html::parse_document(&post);
+    let desc_sel = Selector::parse("meta[property=\"og:description\"]").unwrap();
+    let desc_elem = match doc.select(&desc_sel).next() {
+        None => return Ok(None),
+        Some(elem) => elem,
+    };
+
+    let content = match desc_elem.value().attr("content") {
+        None => return Ok(None),
+        Some(content) => content.to_string(),
+    };
+
+    return Ok(Some(content));
+}
+
+#[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> {
+    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(true)));
+
+        // 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(true),
+        ));
+    }
+
+    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(true),
+        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 post = fetch_post(&link, true)?;
+    let mut msg = parse_tgmessage(&post)?;
+
+    if msg.message.is_none() {
+        msg.message = fetch_fallback(&link)?;
+    }
+
+    if let Some(message) = &msg.message {
+        if link.translated {
+            println!("translating {}#{}", link.username, link.message_id);
+            msg.message = Some(fetch_translation(message)?);
+        }
+    }
+
+    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")?,
+        translated: false,
+    };
+
+    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 mut raw_url = request.raw_url();
+        let mut translate = false;
+
+        let response = loop {
+            if raw_url.starts_with("/img/") {
+                break handle_img_redirect(&cache, &raw_url[5..]);
+            }
+
+            if raw_url.starts_with("/translate/") {
+                translate = true;
+                raw_url = &raw_url[10..];
+            }
+
+            break match TgLink::parse(raw_url, translate) {
+                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.
+
+it can also translate posts from russian, ukrainian or whatever other
+dumb language you speak into english, by adding `/translate/`, for
+example:
+
+  https://tgsa.tazj.in/translate/https://t.me/strelkovii/4329
+
+expect this to be slow though. that's the price to pay for translating
+shitty slang.
+
+pm me on the forums if any of 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 000000000000..1b904c06fadf
--- /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 000000000000..ad6ec446f661
--- /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 000000000000..f7477f168914
--- /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 000000000000..efa263790b45
--- /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 000000000000..e6b83bdcd430
--- /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 000000000000..f88d237e2b91
--- /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 000000000000..848bf62bd7ed
--- /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 000000000000..6002ad695a1e
--- /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 000000000000..eb4d3f1bfa33
--- /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 000000000000..d2c4da80187c
--- /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 000000000000..8df5b1132e74
--- /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 000000000000..3274a3a2d21d
--- /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 000000000000..62a6debf476f
--- /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 000000000000..e454072ac42a
--- /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 000000000000..502f5dac903e
--- /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 000000000000..3d4115305d10
--- /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 000000000000..b8246618bebc
--- /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 000000000000..d64a1cb867ec
--- /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 000000000000..f8cb6107f30c
--- /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 000000000000..335efb057172
--- /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 000000000000..55e6c22ad212
--- /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 000000000000..887c891da36c
--- /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 000000000000..f0811418dff5
--- /dev/null
+++ b/users/tvlbot.jpg
Binary files differdiff --git a/users/wpcarro/.envrc b/users/wpcarro/.envrc
new file mode 100644
index 000000000000..3e1e1a35f013
--- /dev/null
+++ b/users/wpcarro/.envrc
@@ -0,0 +1,5 @@
+source_up
+
+export PATH="${PWD}/bin:${PATH}"
+export REPO_ROOT=$(git rev-parse --show-toplevel)
+export WPCARRO="${REPO_ROOT}/users/wpcarro"
diff --git a/users/wpcarro/.gitignore b/users/wpcarro/.gitignore
new file mode 100644
index 000000000000..64703ed12903
--- /dev/null
+++ b/users/wpcarro/.gitignore
@@ -0,0 +1,35 @@
+.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
+
+# Nix gcroots symlinks created by .envrc
+/.gcroots/*
diff --git a/users/wpcarro/.gitsecret/keys/pubring.kbx b/users/wpcarro/.gitsecret/keys/pubring.kbx
new file mode 100644
index 000000000000..692d5c67b04b
--- /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 000000000000..c0a748ce2c37
--- /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 000000000000..369485be0624
--- /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 000000000000..fda2c84fb3d8
--- /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 000000000000..eb63db6c4016
--- /dev/null
+++ b/users/wpcarro/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+wpcarro
diff --git a/users/wpcarro/README.md b/users/wpcarro/README.md
new file mode 100644
index 000000000000..be0aacf247f5
--- /dev/null
+++ b/users/wpcarro/README.md
@@ -0,0 +1,47 @@
+# 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:~/.passage' ~/.passage
+```
diff --git a/users/wpcarro/assessments/brilliant/.ghci b/users/wpcarro/assessments/brilliant/.ghci
new file mode 100644
index 000000000000..efc88e630ccb
--- /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 000000000000..0272988f371c
--- /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 000000000000..13b5de0145aa
--- /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 000000000000..e94c73bea287
--- /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 000000000000..60d7de4e25ae
--- /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 000000000000..e99e025641fa
--- /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 000000000000..d8df8f8372e0
--- /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 000000000000..c69d00333b8e
--- /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 000000000000..0628679c0127
--- /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 000000000000..e08399c093e1
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..b100af4432c5
--- /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 000000000000..44f91e2b2311
--- /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 000000000000..3d13da1cb18e
--- /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 000000000000..b5d604085b9b
--- /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 000000000000..a831fa95c08e
--- /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 000000000000..0af71d1f5b56
--- /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 000000000000..b9e871eced86
--- /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 000000000000..868668ca501c
--- /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 000000000000..d684679d264f
--- /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 000000000000..bfcced43b930
--- /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 000000000000..d0d948402001
--- /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 000000000000..28060bfb3c40
--- /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 000000000000..b5b25bd64822
--- /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 000000000000..7d5a15482ab3
--- /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 000000000000..d2cda26eddc9
--- /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 000000000000..db9704a85ff6
--- /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 000000000000..7a6fa5ea248d
--- /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 000000000000..cd064cc1ae4b
--- /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 000000000000..f0c2ea4b9e21
--- /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 000000000000..8a33be475389
--- /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 000000000000..cb55520920de
--- /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 000000000000..7ab5e905b5a0
--- /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 000000000000..13a6ab374ff6
--- /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 000000000000..9062f927e7a8
--- /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 000000000000..2ae7efbb3fb6
--- /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 000000000000..67d0b8875cae
--- /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 000000000000..c7186c824a8c
--- /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 000000000000..08d559734b5a
--- /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 000000000000..869559e709ea
--- /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 000000000000..d4d62d436b26
--- /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 000000000000..0231ef3ab8a4
--- /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 000000000000..1cb4f3034cc3
--- /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 000000000000..04804ad94fac
--- /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 000000000000..c4095e118e24
--- /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 000000000000..52114e0e9fb0
--- /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 000000000000..9e6cef70dbb4
--- /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 000000000000..3cfb279230cb
--- /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 000000000000..78f55385db9a
--- /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 000000000000..d95609ee15e4
--- /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 000000000000..63ba97b794ac
--- /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 000000000000..b1a436098afd
--- /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 000000000000..de71a72db0df
--- /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 000000000000..cd15c99a34a8
--- /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 000000000000..addb0a4ffd12
--- /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 000000000000..b3f78bb16980
--- /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 000000000000..57d419db5a82
--- /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 000000000000..7f8f379795f7
--- /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 000000000000..87871b78dbc4
--- /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 000000000000..60343cd87018
--- /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 000000000000..f5fc77b6d77f
--- /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 000000000000..a583c750f77c
--- /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 000000000000..e200d2b49c02
--- /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 000000000000..bf8486ba1d92
--- /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 000000000000..efc88e630ccb
--- /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 000000000000..471fa761e0f4
--- /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 000000000000..c7ab7a2f135f
--- /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 000000000000..742bc962dc55
--- /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 000000000000..f1bff23257e0
--- /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 000000000000..2dac0973ba6d
--- /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 000000000000..0c700470f3e2
--- /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 000000000000..d78e12e3fd8a
--- /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 000000000000..9df4232066bb
--- /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 000000000000..a555185fa717
--- /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 000000000000..713059a38383
--- /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 000000000000..f90740363c52
--- /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 000000000000..6b06a39694fc
--- /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 000000000000..48c33af0796d
--- /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 000000000000..b42753ae5d01
--- /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 000000000000..8c2a66bc8bd7
--- /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 000000000000..39592d04826b
--- /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/boilerplate/README.md b/users/wpcarro/boilerplate/README.md
new file mode 100644
index 000000000000..aa72266a337a
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..f24c5e393a28
--- /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 000000000000..53590850bcd9
--- /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 000000000000..54e34eab7a5d
--- /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 000000000000..8b92b592e1ac
--- /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 000000000000..f6b60dba404e
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..1cb4f3034cc3
--- /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 000000000000..04804ad94fac
--- /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 000000000000..a95f80408ec4
--- /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 000000000000..b5c61c956711
--- /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 000000000000..ce8f727b6f3b
--- /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 000000000000..afcc0f4d3645
--- /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 000000000000..00bb9e281af4
--- /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 000000000000..27f1d811a89a
--- /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 000000000000..30006460cde9
--- /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 000000000000..c1edae8bb638
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..ebea22e071dd
--- /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 000000000000..a54186a9f29e
--- /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 000000000000..84949cae7f3c
--- /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 000000000000..104e7272da93
--- /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 000000000000..d68fa618664e
--- /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 000000000000..a3ae929ef446
--- /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 000000000000..4fae1b36ac4d
--- /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 000000000000..b5c61c956711
--- /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 000000000000..91752af916a4
--- /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 000000000000..dc28dc4a9cc8
--- /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 000000000000..03e980a491cc
--- /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 000000000000..af829e20f9cb
--- /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 000000000000..013f34fdf0b1
--- /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 000000000000..0e16fe80a47c
--- /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 000000000000..2f0fd9e1c2d1
--- /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 000000000000..09b8990e13e2
--- /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 000000000000..cbf58a1e744b
--- /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 000000000000..8211cbf08fb6
--- /dev/null
+++ b/users/wpcarro/common.nix
@@ -0,0 +1,83 @@
+{ 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 = {
+    # Remodel the system clipboard as a list instead of a point.
+    clipmenu.enable = true;
+
+    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; [
+    age
+    bat
+    coreutils
+    direnv
+    diskus
+    emacs
+    exa
+    fd
+    fzf
+    git
+    gnupg
+    htop
+    jq
+    nmap
+    passage
+    python3
+    rink
+    ripgrep
+    tldr
+    tokei
+    tree
+    vim
+    whois
+    # TODO(wpcarro): Debug this failing build.
+    # wpcarro.tools.simple_vim
+    xclip
+    zip
+  ] ++
+  (if pkgs.stdenv.isLinux then [
+    mkpasswd
+    sysz
+    # This depends on compiler-rt-libc-10.0.1, which is marked as broken on
+    # aarch64-darwin, but depot sets `allowBroken = true`, which means any
+    # build that depends on dig will fail on OSX (e.g. emacs).
+    # https://cs.tvl.fyi/github.com/NixOS/nixpkgs@e9b195248c6cd7961a453b10294aea9ab58e01b4/-/blob/pkgs/development/compilers/llvm/10/compiler-rt/default.nix?L122
+    dig
+  ] else [ ]);
+}
diff --git a/users/wpcarro/configs/.config/nixpkgs/config.nix b/users/wpcarro/configs/.config/nixpkgs/config.nix
new file mode 100644
index 000000000000..1dd1750ae025
--- /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 000000000000..57cfe7ea6a20
--- /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 000000000000..949743d72587
--- /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 000000000000..c83adbc69fa0
--- /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 000000000000..fac317f3f072
--- /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 000000000000..387f2023d2d2
--- /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 000000000000..2f79ed6ccaa8
--- /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 000000000000..a073da25ea82
--- /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 000000000000..e9f2cab3bcec
--- /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/.gnupg/crls.d/DIR.txt b/users/wpcarro/configs/.gnupg/crls.d/DIR.txt
new file mode 100644
index 000000000000..2a29a47b8dbb
--- /dev/null
+++ b/users/wpcarro/configs/.gnupg/crls.d/DIR.txt
@@ -0,0 +1 @@
+v:1:
diff --git a/users/wpcarro/configs/.gnupg/exported/ownertrust.txt b/users/wpcarro/configs/.gnupg/exported/ownertrust.txt
new file mode 100644
index 000000000000..79b727914f84
--- /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 000000000000..8b5547f4c33b
--- /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/pubring.kbx b/users/wpcarro/configs/.gnupg/pubring.kbx
new file mode 100644
index 000000000000..208fad71b797
--- /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 000000000000..8781b2ad9b05
--- /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 000000000000..7e8b3e3fb4a9
--- /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 000000000000..101495c3ef0b
--- /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 000000000000..681f976052c7
--- /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 000000000000..a3f3ec328e16
--- /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 000000000000..9650479c420e
--- /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 000000000000..3be8fefd9c1e
--- /dev/null
+++ b/users/wpcarro/dotfiles/config.fish
@@ -0,0 +1,44 @@
+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 gcan 'git commit --amend --no-edit'
+alias gco 'git checkout'
+alias gd 'git diff'
+alias gds 'git diff --staged'
+alias glp 'git log --pretty --oneline --graph'
+alias gpf 'git push --force'
+alias gsh 'git show HEAD'
+alias gst 'git status'
+alias gprom 'git pull --rebase origin main'
+alias gfom 'git fetch origin main'
+alias grh 'git reset --hard'
+alias gproc 'git pull --rebase origin canon'
+alias edit 'emacsclient -n'
+alias h 'cd /hadrian'
+alias d 'cd /depot'
+alias hw 'cd /hadrian/users/wpcarro'
+alias dw 'cd /depot/users/wpcarro'
+alias sc 'systemctl'
+alias ef 'edit ~/.config/fish/config.fish'
+alias sf 'source ~/.config/fish/config.fish'
+alias tf 'terraform'
+
+# 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 000000000000..8150f5370699
--- /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 000000000000..a17533f07330
--- /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/gitconfig b/users/wpcarro/dotfiles/gitconfig
new file mode 100644
index 000000000000..f81c0c40f745
--- /dev/null
+++ b/users/wpcarro/dotfiles/gitconfig
@@ -0,0 +1,9 @@
+[user]
+	name = "William Carroll"
+	email = "wpcarro@gmail.com"
+[diff]
+  external = difft
+[push]
+	default = current
+[rebase]
+	autoStash = true
diff --git a/users/wpcarro/dotfiles/prompt.fish b/users/wpcarro/dotfiles/prompt.fish
new file mode 100644
index 000000000000..58d22dab5efa
--- /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 000000000000..5db74d36c70b
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/init.el
@@ -0,0 +1,15 @@
+;; load order is intentional
+(setq-default debug-on-error t)
+(require 'wpc-package)
+(require 'wpc-misc)
+(require 'ssh)
+(require 'keyboard)
+(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 000000000000..a23addefafe4
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..52bc717e470e
--- /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 000000000000..5d44e8ed7989
--- /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 000000000000..6e9282f83c79
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..8ea7b8f07724
--- /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 000000000000..bfa888d5265d
--- /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 000000000000..bf6e525f8c65
--- /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 000000000000..0f0ad5c4fc4e
--- /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 000000000000..2a0bcc33f7bb
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..95f7d9deecd0
--- /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 000000000000..4c4db62a8a47
--- /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 000000000000..10d194ce41f0
--- /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 000000000000..9d6084acb40d
--- /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 000000000000..1ab0d762b611
--- /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 000000000000..7609f801f278
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..3cea6ce003ba
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..1839a27eb5c0
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..7f110a9718e4
--- /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 000000000000..b1769cd3d102
--- /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 000000000000..a3c236821e06
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..b5eb5a244721
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..4215b15992b6
--- /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 000000000000..ac65ea2e49be
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..4dd22dc0b2da
--- /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 000000000000..379ceda1a3a6
--- /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 000000000000..db48adfec737
--- /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 000000000000..5c407495f53a
--- /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 000000000000..0f45ae782d32
--- /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 000000000000..3babc730305a
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..882c48ded39d
--- /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 000000000000..b9a684588bc4
--- /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 000000000000..254b9fd96b18
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..6b4b6a5db2a7
--- /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 000000000000..40f34ff8d1f1
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..62834a29ab04
--- /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 000000000000..82ec3fd8e379
--- /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 000000000000..8e35e61fc2c4
--- /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 000000000000..13f2018f2269
--- /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 000000000000..2a52c57c75cd
--- /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 000000000000..187a2efc5a7c
--- /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 000000000000..694914a83c95
--- /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 000000000000..ded3ce163a93
--- /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 000000000000..0463f5cd5593
--- /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 000000000000..fcd51f687b61
--- /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 000000000000..09fa6df50506
--- /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 000000000000..9f550e300d12
--- /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 000000000000..ed382d4f74c4
--- /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 000000000000..12ca2e786ded
--- /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 000000000000..f2a93a31d96d
--- /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 000000000000..681c5d0dfdf4
--- /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 000000000000..53c6e5fc5ac2
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..4d8e0e3bbd24
--- /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 000000000000..bf0e876e2b98
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..efa946bb272f
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..797781968881
--- /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 000000000000..bc3c356a6157
--- /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 000000000000..d58dacb7a0b1
--- /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 000000000000..ae59c7a50f9c
--- /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 000000000000..b791cdf86fe5
--- /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 000000000000..6d5f86f8b4e6
--- /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/buffer.el b/users/wpcarro/emacs/.emacs.d/wpc/buffer.el
new file mode 100644
index 000000000000..0f86f7f811e6
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/buffer.el
@@ -0,0 +1,174 @@
+;;; 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)
+(require 'list)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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/clipboard.el b/users/wpcarro/emacs/.emacs.d/wpc/clipboard.el
new file mode 100644
index 000000000000..ec2a46f5404f
--- /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/constants.el b/users/wpcarro/emacs/.emacs.d/wpc/constants.el
new file mode 100644
index 000000000000..48bcd9042f78
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/constants.el
@@ -0,0 +1,29 @@
+;;; 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"))
+  "Defined as t when Emacs is running in CI.")
+
+(defconst constants-osx? (eq system-type 'darwin)
+  "Defined as t when OSX is running.")
+
+(provide 'constants)
+;;; constants.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 000000000000..69dae6939e40
--- /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/email.el b/users/wpcarro/emacs/.emacs.d/wpc/email.el
new file mode 100644
index 000000000000..a83ca25e6c17
--- /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 000000000000..0f70f69c2b8d
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/fonts.el
@@ -0,0 +1,99 @@
+;;; 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:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'maybe)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgroup fonts nil
+  "Customize group for fonts configuration.")
+
+(defcustom fonts-size "10"
+  "My preferred default font-size."
+  :group 'fonts)
+
+(defcustom fonts-size-step 10
+  "The amount (%) by which to increase or decrease a font."
+  :group 'fonts)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(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-current ()
+  "Return the currently enabled font."
+  (symbol-name (font-get (face-attribute 'default :font) :family)))
+
+(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-set (fonts-current) fonts-size))
+
+(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/ivy-helpers.el b/users/wpcarro/emacs/.emacs.d/wpc/ivy-helpers.el
new file mode 100644
index 000000000000..3303237d52af
--- /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 000000000000..7defc3d08f3b
--- /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 000000000000..a55bf2733011
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/keybindings.el
@@ -0,0 +1,495 @@
+;;; 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 '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 'fonts)
+(require 'bookmark)
+(require 'tvl)
+(require 'window-manager)
+
+;; 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)
+(require 'passage)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helper Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun keybindings--window-vsplit-right ()
+  "Split the window vertically and focus the right half."
+  (interactive)
+  (evil-window-vsplit)
+  (windmove-right))
+
+(defun keybindings--window-split-down ()
+  "Split the window horizontal and focus the bottom half."
+  (interactive)
+  (evil-window-split)
+  (windmove-down))
+
+(defun keybindings--create-snippet ()
+  "Create a window split and then opens the Yasnippet editor."
+  (interactive)
+  (evil-window-vsplit)
+  (call-interactively #'yas-new-snippet))
+
+(defun keybindings--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 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))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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"  #'keybindings--window-vsplit-right
+  "sh"  #'evil-window-vsplit
+  "sk"  #'evil-window-split
+  "sj"  #'keybindings--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)))))
+
+(general-define-key
+ :keymaps '(isearch-mode-map)
+ "C-p" #'isearch-ring-retreat
+ "C-n" #'isearch-ring-advance
+ "<up>" #'isearch-ring-retreat
+ "<down>" #'isearch-ring-advance)
+
+(general-define-key
+ :keymaps '(minibuffer-local-isearch-map)
+ "C-p" #'previous-line-or-history-element
+ "C-n" #'next-line-or-history-element
+ "<up>" #'previous-line-or-history-element
+ "<down>" #'next-line-or-history-element)
+
+(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-\\" #'passage-select)
+
+(defun keybindings-copy-emoji ()
+  "Select an emoji from the completing-read menu."
+  (interactive)
+  (clipboard-copy (emojify-completing-read "Copy: ")))
+
+(keybindings-exwm "s-e" #'keybindings-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" #'keybindings--create-snippet
+ "l" #'linum-mode
+ "B" #'magit-blame
+ "w" #'save-buffer
+ "r" #'keybindings--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>" "+"
+ "M--" #'evil-window-split
+ "M-\\" #'evil-window-vsplit)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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)
+
+(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-k" #'kill-line
+ "C-u" (lambda () (interactive) (kill-line 0))
+ "C-v" #'clipboard-yank
+ "C-S-v" #'clipboard-yank)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Rust
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(general-define-key
+ :keymaps '(rust-mode-map)
+ :states '(normal)
+ "gd" #'lsp-find-definition
+ "gr" #'lsp-find-references)
+
+(general-define-key
+ :keymaps '(rust-mode-map)
+ "TAB" #'company-indent-or-complete-common)
+
+(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 000000000000..0ee00e1b84e7
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/keyboard.el
@@ -0,0 +1,138 @@
+;;; 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 'cl-lib)
+(require 'prelude)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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 (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 (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 (1+ keyboard-repeat-rate))
+  (keyboard-set-key-repeat :rate keyboard-repeat-rate)
+  (keyboard-message
+   (format "Rate: %s" keyboard-repeat-rate)))
+
+(defun keyboard-dec-repeat-rate ()
+  "Decrement `keyboard-repeat-rate'."
+  (interactive)
+  (setq keyboard-repeat-rate (1- keyboard-repeat-rate))
+  (keyboard-set-key-repeat :rate keyboard-repeat-rate)
+  (keyboard-message
+   (format "Rate: %s" keyboard-repeat-rate)))
+
+(defun keyboard-inc-repeat-delay ()
+  "Increment `keyboard-repeat-delay'."
+  (interactive)
+  (setq keyboard-repeat-delay (1+ keyboard-repeat-delay))
+  (keyboard-set-key-repeat :delay keyboard-repeat-delay)
+  (keyboard-message
+   (format "Delay: %s" keyboard-repeat-delay)))
+
+(defun keyboard-dec-repeat-delay ()
+  "Decrement `keyboard-repeat-delay'."
+  (interactive)
+  (setq keyboard-repeat-delay (1- keyboard-repeat-delay))
+  (keyboard-set-key-repeat :delay keyboard-repeat-delay)
+  (keyboard-message
+   (format "Delay: %s" keyboard-repeat-delay)))
+
+(defun keyboard-print-key-repeat ()
+  "Print the currently set values for key repeat."
+  (interactive)
+  (keyboard-message
+   (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/modeline.el b/users/wpcarro/emacs/.emacs.d/wpc/modeline.el
new file mode 100644
index 000000000000..df1cddec9d92
--- /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/prelude.el b/users/wpcarro/emacs/.emacs.d/wpc/prelude.el
new file mode 100644
index 000000000000..4a332cb8ca0e
--- /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 000000000000..eaa610659073
--- /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/region.el b/users/wpcarro/emacs/.emacs.d/wpc/region.el
new file mode 100644
index 000000000000..0b692981f899
--- /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/screen-brightness.el b/users/wpcarro/emacs/.emacs.d/wpc/screen-brightness.el
new file mode 100644
index 000000000000..851be9f99f80
--- /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 000000000000..08994fea5fda
--- /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/ssh.el b/users/wpcarro/emacs/.emacs.d/wpc/ssh.el
new file mode 100644
index 000000000000..1179e9036334
--- /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/window-manager.el b/users/wpcarro/emacs/.emacs.d/wpc/window-manager.el
new file mode 100644
index 000000000000..94fb99d4271b
--- /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/wpc-clojure.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-clojure.el
new file mode 100644
index 000000000000..5582641b3f35
--- /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 000000000000..89fde4b6a177
--- /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 000000000000..7c22a4657fc0
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-dired.el
@@ -0,0 +1,51 @@
+;;; 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)
+  (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-dotnet.el b/users/wpcarro/emacs/.emacs.d/wpc/wpc-dotnet.el
new file mode 100644
index 000000000000..03fc430e4846
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-dotnet.el
@@ -0,0 +1,16 @@
+;;; wpc-dotnet.el --- C# and company -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+
+;;; Commentary:
+;; Windows things v0v.
+
+;;; Code:
+
+(require 'macros)
+
+(use-package csharp-mode)
+(macros-support-file-extension "csproj" xml-mode)
+
+(provide 'wpc-dotnet)
+;;; wpc-dotnet.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 000000000000..69259274c86d
--- /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 000000000000..c32c5daeff13
--- /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 000000000000..47198c8e02a1
--- /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 000000000000..536790e36d61
--- /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 000000000000..9e137ad8803e
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-javascript.el
@@ -0,0 +1,93 @@
+;;; 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)
+
+(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 000000000000..8363e3c08ea0
--- /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)
+(require 'wpc-dotnet)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Miscellaneous Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(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 000000000000..599d42620419
--- /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 000000000000..36fbf8b12ce6
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-misc.el
@@ -0,0 +1,330 @@
+;;; 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)
+
+(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)
+
+;; 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)))
+
+;; Always auto-close parantheses and other pairs
+(electric-pair-mode)
+
+;; 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 000000000000..e9dc203691c2
--- /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 000000000000..229177220b1e
--- /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 000000000000..9c57bb427076
--- /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 000000000000..6779431c1215
--- /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 000000000000..9ffb7c4c8a2b
--- /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 000000000000..b609efb431fd
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-rust.el
@@ -0,0 +1,30 @@
+;;; 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.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'lsp)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(use-package rust-mode
+  :config
+  (setq lsp-rust-server #'rust-analyzer)
+  (setq rust-format-show-buffer nil)
+  (setq rust-format-on-save t)
+  (add-hook 'rust-mode-hook #'lsp))
+
+(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 000000000000..f4229ed328e7
--- /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 000000000000..a2f533cec095
--- /dev/null
+++ b/users/wpcarro/emacs/.emacs.d/wpc/wpc-ui.el
@@ -0,0 +1,186 @@
+;;; 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 '>)
+(require 'al)
+(require 'constants)
+(require 'dash)
+(require 'fonts)
+(require 'general)
+(require 'modeline)
+(require 'prelude)
+(require 'theme)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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)
+
+;; 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)))
+
+;; all-the-icons
+(use-package all-the-icons
+  :config
+  (unless (or constants-ci?
+              (f-exists? "~/.local/share/fonts/all-the-icons.ttf")
+              (f-exists? "~/Library/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 tabs))
+  (global-whitespace-mode t))
+
+;; 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))
+
+(display-battery-mode 1)
+
+(setq theme-whitelist
+      (->> (custom-available-themes)
+           (list-map #'symbol-name)
+           (list-filter (>-> (s-starts-with? "doom-")))
+           (list-map #'intern)
+           cycle-from-list))
+(setq theme-linum-color-override "da5478")
+(add-hook 'theme-after-change
+          (lambda () (prelude-set-line-number-color "#da5478")))
+(theme-whitelist-set 'doom-flatwhite)
+
+(when window-system
+  ;; On OSX, JetBrainsMono is installed as "JetBrains Mono", and I'm
+  ;; not sure how to change that.
+  (let ((font (if constants-osx? "JetBrains Mono" "JetBrainsMono")))
+    (fonts-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/AppIcon.icns b/users/wpcarro/emacs/AppIcon.icns
new file mode 100644
index 000000000000..b3be251ccf8e
--- /dev/null
+++ b/users/wpcarro/emacs/AppIcon.icns
Binary files differdiff --git a/users/wpcarro/emacs/README.md b/users/wpcarro/emacs/README.md
new file mode 100644
index 000000000000..16f4fc31f989
--- /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 000000000000..9dfaf3056fdc
--- /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 000000000000..78c66c693d09
--- /dev/null
+++ b/users/wpcarro/emacs/default.nix
@@ -0,0 +1,265 @@
+# My Emacs distribution, which is supporting the following platforms:
+# - Linux
+# - Darwin
+#
+# USAGE:
+#   $ nix-build -A users.wpcarro.emacs.osx -o /Applications/BillsEmacs.app
+{ 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 ++
+    # Rust dependencies
+    (with pkgs; [
+      cargo
+      rust-analyzer
+      rustc
+      rustfmt
+    ]) ++
+    # Misc dependencies
+    (with pkgs; [
+      ispell
+      nix
+      rust-analyzer
+      rustc
+      rustfmt
+      xorg.xset
+    ] ++
+    (if pkgs.stdenv.isLinux then [
+      scrot
+    ] else [ ]))
+  );
+
+  emacsWithPackages = (emacsPackagesFor emacs28).emacsWithPackages;
+
+  wpcarrosEmacs = emacsWithPackages (epkgs:
+    (with wpcarro.emacs.pkgs; [
+      al
+      bookmark
+      cycle
+      list
+      macros
+      maybe
+      passage
+      set
+      string
+      struct
+      symbol
+      theme
+      tuple
+      vterm-mgt
+      zle
+    ]) ++
+
+    (with epkgs.tvlPackages; [
+      tvl
+    ]) ++
+
+    (with epkgs.elpaPackages; [
+      exwm
+    ]) ++
+
+    (with epkgs.melpaPackages; [
+      alert
+      all-the-icons
+      all-the-icons-ivy
+      avy
+      base16-theme
+      cider
+      clojure-mode
+      company
+      counsel
+      counsel-projectile
+      csharp-mode
+      dap-mode
+      dash
+      deadgrep
+      deferred
+      diminish
+      direnv
+      dockerfile-mode
+      doom-themes
+      elisp-slime-nav
+      elixir-mode
+      elm-mode
+      emojify
+      engine-mode
+      evil
+      evil-collection
+      evil-commentary
+      evil-surround
+      f
+      fish-mode
+      flycheck
+      flymake-shellcheck
+      general
+      go-mode
+      haskell-mode
+      helpful
+      ivy
+      ivy-clipmenu
+      ivy-prescient
+      key-chord
+      lispyville
+      lsp-ui
+      magit
+      magit-popup
+      markdown-mode
+      nix-mode
+      notmuch
+      org-bullets
+      package-lint
+      paradox
+      parsec
+      pcre2el
+      prettier-js
+      projectile
+      py-yapf
+      racket-mode
+      rainbow-delimiters
+      reason-mode
+      refine
+      request
+      restclient
+      rjsx-mode
+      rust-mode
+      sly
+      suggest
+      telephone-line
+      terraform-mode
+      tide
+      ts
+      tuareg
+      use-package
+      vterm
+      web-mode
+      which-key
+      yaml-mode
+      yasnippet
+    ]) ++
+
+    [
+      epkgs.eglot # from elpa devel
+    ]);
+
+  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)}
+        "$@"
+    '';
+
+  # I can't figure out how to augment LSEnvironment.PATH such that it inherits
+  # the default $PATH and adds the things that I need as well, so let's
+  # hardcode the desired outcome in the meantime.
+  osxDefaultPath = builtins.concatStringsSep ":" [
+    "/Users/bill/.nix-profile/bin"
+    "/nix/var/nix/profiles/default/bin"
+    "/opt/homebrew/bin"
+    "/opt/homebrew/sbin"
+    "/usr/local/bin"
+    "/usr/bin"
+    "/bin"
+    "/usr/sbin"
+    "/sbin"
+    "/opt/X11/bin"
+  ];
+
+  infoPlist = pkgs.writeText "Info.plist" (pkgs.lib.generators.toPlist { } {
+    LSEnvironment = {
+      PATH = "${emacsBinPath}:${osxDefaultPath}";
+    };
+    CFBundleExecutable = "BillsEmacs";
+    CFBundleDisplayName = "BillsEmacs";
+    CFBundleIconFile = "AppIcon";
+    CFBundleIconName = "AppIcon";
+  });
+
+  versionPlist = pkgs.writeText "version.plist" (pkgs.lib.generators.toPlist { } {
+    ProjectName = "OSXPlatformSupport";
+  });
+in
+{
+  # TODO(wpcarro): Support this with base.overrideAttrs or something similar.
+  nixos = { load ? [ ] }: withEmacsPath {
+    inherit load;
+    emacsBin = "${wpcarrosEmacs}/bin/emacs";
+  };
+
+  # To install GUI:
+  # $ nix-build -A users.wpcarro.emacs.osx -o /Applications/BillsEmacs.app
+  osx = pkgs.stdenv.mkDerivation {
+    pname = "bills-emacs";
+    version = "0.0.1";
+    src = ./.;
+    dontFixup = true;
+    installPhase = ''
+      runHook preInstall
+      APP="$out"
+      mkdir -p "$APP/Contents/MacOS"
+      mkdir -p "$APP/Contents/Resources"
+      cp ${infoPlist}      "$APP/Contents/Info.plist"
+      cp ${versionPlist}   "$APP/Contents/version.plist"
+      cp ${./AppIcon.icns} "$APP/Contents/Resources/AppIcon.icns"
+      echo "APPL????"  > "$APP/Contents/PkgInfo"
+      cat << EOF > "$APP/Contents/MacOS/BillsEmacs"
+      #!${pkgs.stdenvNoCC.shell}
+      export EMACSLOADPATH="${loadPath}"
+      exec ${wpcarrosEmacs}/bin/emacs \
+        --debug-init \
+        --no-init-file \
+        --no-site-file \
+        --no-site-lisp \
+        --load ${./.emacs.d/init.el}
+      EOF
+      chmod +x "$APP/Contents/MacOS/BillsEmacs"
+      runHook postInstall
+    '';
+    meta.platforms = [ "aarch64-darwin" ];
+  };
+
+  # 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 000000000000..0e39c3069d8b
--- /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 000000000000..96ba7c96459b
--- /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/pkgs/al/al.el b/users/wpcarro/emacs/pkgs/al/al.el
new file mode 100644
index 000000000000..4c37526c644a
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/al/al.el
@@ -0,0 +1,227 @@
+;;; 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 'dash)
+(require 'list)
+(require 'map)
+
+;; 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)'.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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 &optional default)
+  "Return the value at K in XS; otherwise, return nil or DEFAULT (if set).
+Returns the first occurrence of K in XS since alists support multiple entries."
+  (if (not (al-has-key? k xs))
+      default
+    (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."
+  (not (eq nil (assoc k xs))))
+
+(defun al-has-value? (v xs)
+  "Return t if XS has a value of V."
+  (not (eq nil (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))
+
+(provide 'al)
+;;; al.el ends here
diff --git a/users/wpcarro/emacs/pkgs/al/default.nix b/users/wpcarro/emacs/pkgs/al/default.nix
new file mode 100644
index 000000000000..d88e0757a875
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/al/default.nix
@@ -0,0 +1,28 @@
+{ pkgs, depot, ... }:
+
+let
+  al = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "al";
+        version = "1.0.0";
+        src = ./al.el;
+        packageRequires =
+          (with emacsPackages; [
+            dash
+          ]) ++
+          (with depot.users.wpcarro.emacs.pkgs; [
+            list
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [ al ]);
+in
+al.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/al/tests.el b/users/wpcarro/emacs/pkgs/al/tests.el
new file mode 100644
index 000000000000..04fe4dcbb5a6
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/al/tests.el
@@ -0,0 +1,53 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'al)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest al-has-key? ()
+  (should (al-has-key? 'fname '((fname . "William"))))
+  (should (not (al-has-key? 'lname '((fname . "William"))))))
+
+(ert-deftest al-get ()
+  (let ((xs (->> (al-new)
+                 (al-set 'fname "John")
+                 (al-set 'employed? nil))))
+    (should (string= "John" (al-get 'fname xs)))
+    (should (string= "Cleese" (al-get 'lname xs "Cleese")))
+    ;; Test that the value of nil is returned even when a default is defined,
+    ;; which could be a subtle bug in the typical Elisp pattern of supporting
+    ;; defaults with: (or foo default).
+    (should (eq nil (al-get 'employed? xs)))
+    (should (eq nil (al-get 'employed? xs "default")))))
+
+(ert-deftest al-has-value? ()
+  (should (al-has-value? "William" '((fname . "William"))))
+  (should (not (al-has-key? "John" '((fname . "William"))))))
+
+(ert-deftest al-map-keys ()
+  (should
+   (equal '((2 . one)
+            (3 . two))
+          (al-map-keys #'1+
+                       '((1 . one)
+                         (2 . two))))))
+
+(ert-deftest al-map-values ()
+  (should (equal '((one . 2)
+                   (two . 3))
+                 (al-map-values #'1+
+                                '((one . 1)
+                                  (two . 2))))))
+
+(ert-deftest al-delete ()
+  (let ((person (->> (al-new)
+                     (al-set "fname" "John")
+                     (al-set "lname" "Cleese")
+                     (al-set "age" 82))))
+    (should (al-has-key? "age" person))
+    (should (not (al-has-key? "age" (al-delete "age" person))))))
diff --git a/users/wpcarro/emacs/pkgs/bag/bag.el b/users/wpcarro/emacs/pkgs/bag/bag.el
new file mode 100644
index 000000000000..502f5672536e
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bag/bag.el
@@ -0,0 +1,78 @@
+;;; 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 bad 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 'list)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct bag xs)
+
+(defun bag-new ()
+  "Create an empty bag."
+  (make-bag :xs (al-new)))
+
+(defun bag-from-list (xs)
+  "Map a list of `XS' into a bag."
+  (->> xs
+       (list-reduce (bag-new) #'bag-add)))
+
+(defun bag-add (x xs)
+  "Add X to XS."
+  (if (bag-contains? x xs)
+      (struct-update
+       bag xs (lambda (xs) (al-update x (lambda (x) (+ 1 x)) xs)) xs)
+    (struct-update bag xs (lambda (xs) (al-set x 1 xs)) xs)))
+
+(defun bag-remove (x xs)
+  "Remove X from XS.
+This is a no-op is X doesn't exist in XS."
+  (when (bag-contains? x xs)
+    (struct-update bag xs (lambda (xs) (al-delete x xs)) xs)))
+
+(defun bag-count (x xs)
+  "Return the number of occurrences of X in XS."
+  (al-get x (bag-xs xs) 0))
+
+(defun bag-total (xs)
+  "Return the total number of elements in XS."
+  (->> (bag-xs xs)
+       (al-reduce 0 (lambda (_key v acc) (+ acc v)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun bag-contains? (x xs)
+  "Return t if XS has X."
+  (al-has-key? x (bag-xs xs)))
+
+(provide 'bag)
+;;; bag.el ends here
diff --git a/users/wpcarro/emacs/pkgs/bag/default.nix b/users/wpcarro/emacs/pkgs/bag/default.nix
new file mode 100644
index 000000000000..3dedc27286d1
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bag/default.nix
@@ -0,0 +1,26 @@
+{ pkgs, depot, ... }:
+
+let
+  bag = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "bag";
+        version = "1.0.0";
+        src = ./bag.el;
+        packageRequires =
+          (with depot.users.wpcarro.emacs.pkgs; [
+            al
+            list
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [ bag ]);
+in
+bag.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/bag/tests.el b/users/wpcarro/emacs/pkgs/bag/tests.el
new file mode 100644
index 000000000000..4970f70815c9
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bag/tests.el
@@ -0,0 +1,32 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'bag)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(setq fixture (bag-from-list '(1 1 1 2 2 3)))
+
+(ert-deftest bag-add ()
+  (should (not (bag-contains? 4 fixture)))
+  (should (bag-contains? 4 (bag-add 4 fixture))))
+
+(ert-deftest bag-remove ()
+  (should (bag-contains? 1 fixture))
+  (should (not (bag-contains? 3 (bag-remove 3 fixture)))))
+
+(ert-deftest bag-count ()
+  (should (= 3 (bag-count 1 fixture)))
+  (should (= 2 (bag-count 2 fixture)))
+  (should (= 1 (bag-count 3 fixture))))
+
+(ert-deftest bag-total ()
+  (should (= 6 (bag-total fixture))))
+
+(ert-deftest bag-contains? ()
+  (should (bag-contains? 1 fixture))
+  (should (not (bag-contains? 4 fixture))))
diff --git a/users/wpcarro/emacs/pkgs/bookmark/bookmark.el b/users/wpcarro/emacs/pkgs/bookmark/bookmark.el
new file mode 100644
index 000000000000..ab9169a078d4
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bookmark/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/pkgs/bookmark/default.nix b/users/wpcarro/emacs/pkgs/bookmark/default.nix
new file mode 100644
index 000000000000..882481701fa2
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bookmark/default.nix
@@ -0,0 +1,13 @@
+{ pkgs, depot, ... }:
+
+pkgs.callPackage
+  ({ emacsPackages }:
+  emacsPackages.trivialBuild {
+    pname = "bookmark";
+    version = "1.0.0";
+    src = ./bookmark.el;
+    packageRequires = (with pkgs.emacsPackages; [
+      general
+    ]);
+  })
+{ }
diff --git a/users/wpcarro/emacs/pkgs/bytes/bytes.el b/users/wpcarro/emacs/pkgs/bytes/bytes.el
new file mode 100644
index 000000000000..b0d64795a074
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bytes/bytes.el
@@ -0,0 +1,94 @@
+;;; 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.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'tuple)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Constants
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defconst bytes-kb (expt 2 10)
+  "Number of bytes in a kilobyte.")
+
+(defconst bytes-mb (expt 2 20)
+  "Number of bytes in a megabytes.")
+
+(defconst bytes-gb (expt 2 30)
+  "Number of bytes in a gigabyte.")
+
+(defconst bytes-tb (expt 2 40)
+  "Number of bytes in a terabyte.")
+
+(defconst bytes-pb (expt 2 50)
+  "Number of bytes in a petabyte.")
+
+(defconst bytes-eb (expt 2 60)
+  "Number of bytes in an exabyte.")
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun bytes-classify (x)
+  "Return unit that closest fits byte count, 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")))))
+    (format "%d%s"
+            (round x (tuple-first base-and-unit))
+            (tuple-second base-and-unit))))
+
+(provide 'bytes)
+;;; bytes.el ends here
diff --git a/users/wpcarro/emacs/pkgs/bytes/default.nix b/users/wpcarro/emacs/pkgs/bytes/default.nix
new file mode 100644
index 000000000000..4e9f52d9b927
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bytes/default.nix
@@ -0,0 +1,25 @@
+{ pkgs, depot, ... }:
+
+let
+  bytes = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "bytes";
+        version = "1.0.0";
+        src = ./bytes.el;
+        packageRequires =
+          (with depot.users.wpcarro.emacs.pkgs; [
+            tuple
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [ bytes ]);
+in
+bytes.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/bytes/tests.el b/users/wpcarro/emacs/pkgs/bytes/tests.el
new file mode 100644
index 000000000000..9b71a466c736
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/bytes/tests.el
@@ -0,0 +1,18 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'bytes)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest bytes-to-string ()
+  (should (equal "1000B" (bytes-to-string 1000)))
+  (should (equal "2KB" (bytes-to-string (* 2 bytes-kb))))
+  (should (equal "17MB" (bytes-to-string (* 17 bytes-mb))))
+  (should (equal "419GB" (bytes-to-string (* 419 bytes-gb))))
+  (should (equal "999TB" (bytes-to-string (* 999 bytes-tb))))
+  (should (equal "2PB" (bytes-to-string (* 2 bytes-pb)))))
diff --git a/users/wpcarro/emacs/pkgs/cycle/README.md b/users/wpcarro/emacs/pkgs/cycle/README.md
new file mode 100644
index 000000000000..416900d2532b
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/cycle/README.md
@@ -0,0 +1,7 @@
+# cycle.el
+
+[![Build status](https://badge.buildkite.com/016bff4b8ae2704a3bbbb0a250784e6692007c582983b6dea7.svg?branch=refs/heads/canon)](https://buildkite.com/tvl/depot)
+
+Cycle data structure exposing mutable and (coming soon!) immutable APIs.
+
+[![asciicast](https://asciinema.org/a/FvFujpRpYjV9qCSGvk3uobOpV.svg)](https://asciinema.org/a/FvFujpRpYjV9qCSGvk3uobOpV)
diff --git a/users/wpcarro/emacs/pkgs/cycle/cycle.el b/users/wpcarro/emacs/pkgs/cycle/cycle.el
new file mode 100644
index 000000000000..2f5b252a0d6a
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/cycle/cycle.el
@@ -0,0 +1,194 @@
+;;; 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 'dash)
+(require 'struct)
+(require 'cl-lib)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Wish list
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; - TODO: Provide immutable variant.
+;; - TODO: Replace mutable consumption with immutable variant.
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; `current-index' tracks the current index
+;; `xs' is the original list
+(cl-defstruct cycle current-index previous-index xs)
+
+(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-previous-focus (cycle)
+  "Return the previously focused entry in CYCLE."
+  (let ((i (cycle-previous-index cycle)))
+    (when i (nth i (cycle-xs cycle)))))
+
+(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 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 (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-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))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun cycle-contains? (x xs)
+  "Return t if cycle, XS, has member X."
+  (not (null (-contains? (cycle-xs xs) 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."
+  (not (null (cycle-current-index xs))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helper Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(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)))
+
+(provide 'cycle)
+;;; cycle.el ends here
diff --git a/users/wpcarro/emacs/pkgs/cycle/default.nix b/users/wpcarro/emacs/pkgs/cycle/default.nix
new file mode 100644
index 000000000000..7ef3b431ada6
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/cycle/default.nix
@@ -0,0 +1,36 @@
+{ pkgs, depot, ... }:
+
+let
+  cycle = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "cycle";
+        version = "1.0.0";
+        src = ./cycle.el;
+        packageRequires =
+          (with emacsPackages; [
+            dash
+          ]) ++
+          (with depot.users.wpcarro.emacs.pkgs; [
+            struct
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    epkgs.dash
+    cycle
+  ]);
+in
+cycle.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+  passthru.meta.ci.extraSteps.github = depot.tools.releases.filteredGitPush {
+    filter = ":/users/wpcarro/emacs/pkgs/cycle";
+    remote = "git@github.com:wpcarro/cycle.el.git";
+    ref = "refs/heads/canon";
+  };
+})
diff --git a/users/wpcarro/emacs/pkgs/cycle/tests.el b/users/wpcarro/emacs/pkgs/cycle/tests.el
new file mode 100644
index 000000000000..29c0e2a0d582
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/cycle/tests.el
@@ -0,0 +1,79 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'cycle)
+(require 'dash)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(setq xs (cycle-new 1 2 3))
+
+(ert-deftest cycle-initializes-properly ()
+  (should (= 3 (cycle-count xs)))
+  (should (null (cycle-previous-focus xs)))
+  (should (cycle-contains? 1 xs))
+  (should (cycle-contains? 2 xs))
+  (should (cycle-contains? 3 xs)))
+
+(ert-deftest cycle-contains? ()
+  ;; Returns t or nil
+  (should (eq t (cycle-contains? 1 xs)))
+  (should (eq t (cycle-contains? 2 xs)))
+  (should (eq t (cycle-contains? 3 xs)))
+  (should (eq nil (cycle-contains? 4 xs))))
+
+(ert-deftest cycle-empty? ()
+  (should (eq t (cycle-empty? (cycle-new))))
+  (should (eq nil (cycle-empty? xs))))
+
+(ert-deftest cycle-current ()
+  (should (= 1 (cycle-current xs))))
+
+(ert-deftest cycle-next! ()
+  (let ((xs (cycle-from-list '(1 2 3))))
+    (should (= 2 (cycle-next! xs)))))
+
+(ert-deftest cycle-prev! ()
+  (let ((xs (cycle-from-list '(1 2 3))))
+    (cycle-next! xs)
+    (should (= 1 (cycle-prev! xs)))))
+
+(ert-deftest cycle-previous-focus ()
+  (let ((xs (cycle-from-list '(1 2 3))))
+    (cycle-focus-item! 2 xs)
+    (cycle-next! xs)
+    (should (= 2 (cycle-previous-focus xs)))))
+
+(ert-deftest cycle-jump! ()
+  (let ((xs (cycle-from-list '(1 2 3))))
+    (should (= 1 (->> xs (cycle-jump! 0) cycle-current)))
+    (should (= 2 (->> xs (cycle-jump! 1) cycle-current)))
+    (should (= 3 (->> xs (cycle-jump! 2) cycle-current)))))
+
+(ert-deftest cycle-focus-previous! ()
+  (let ((xs (cycle-from-list '(1 2 3))))
+    (cycle-focus-item! 2 xs)
+    (cycle-next! xs)
+    (should (= 2 (cycle-previous-focus xs)))
+    (should (= 2 (cycle-focus-previous! xs)))))
+
+(ert-deftest cycle-append! ()
+  (let ((xs (cycle-from-list '(1 2 3))))
+    (cycle-focus-item! 2 xs)
+    (cycle-append! 4 xs)
+    (should (equal '(1 4 2 3) (cycle-xs xs)))))
+
+(ert-deftest cycle-remove! ()
+  (let ((xs (cycle-from-list '(1 2 3))))
+    (should (equal '(1 2) (cycle-xs (cycle-remove! 3 xs))))))
+
+(ert-deftest cycle-misc ()
+  (cycle-focus-item! 3 xs)
+  (cycle-focus-item! 2 xs)
+  (cycle-remove! 1 xs)
+  (should (= 2 (cycle-current xs)))
+  (should (= 3 (cycle-previous-focus xs))))
diff --git a/users/wpcarro/emacs/pkgs/fs/default.nix b/users/wpcarro/emacs/pkgs/fs/default.nix
new file mode 100644
index 000000000000..e6afd107e96b
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/fs/default.nix
@@ -0,0 +1,29 @@
+{ pkgs, depot, ... }:
+
+let
+  fs = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "fs";
+        version = "1.0.0";
+        src = ./fs.el;
+        packageRequires =
+          (with emacsPackages; [
+            dash
+            f
+            s
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    fs
+  ]);
+in
+fs.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/fs/fs.el b/users/wpcarro/emacs/pkgs/fs/fs.el
new file mode 100644
index 000000000000..125c1f1007bd
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/fs/fs.el
@@ -0,0 +1,47 @@
+;;; 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))
+
+(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?)))
+
+(provide 'fs)
+;;; fs.el ends here
diff --git a/users/wpcarro/emacs/pkgs/fs/tests.el b/users/wpcarro/emacs/pkgs/fs/tests.el
new file mode 100644
index 000000000000..adef11a607ae
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/fs/tests.el
@@ -0,0 +1,26 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'fs)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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)))))
diff --git a/users/wpcarro/emacs/pkgs/list/README.md b/users/wpcarro/emacs/pkgs/list/README.md
new file mode 100644
index 000000000000..7afa8494fb7a
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/README.md
@@ -0,0 +1,19 @@
+# list.el
+
+Functions for working with lists in Elisp.
+
+## Wish List
+
+Here are some additional functions that I'd like to support.
+
+-  **TODO**: delete_at/2
+-  **TODO**: flatten/1
+-  **TODO**: flatten/2
+-  **TODO**: foldl/3
+-  **TODO**: foldr/3
+-  **TODO**: insert_at/3
+-  **TODO**: pop_at/3
+-  **TODO**: replace_at/3
+-  **TODO**: starts_with?/2
+-  **TODO**: update_at/3
+-  **TODO**: zip/1
diff --git a/users/wpcarro/emacs/pkgs/list/default.nix b/users/wpcarro/emacs/pkgs/list/default.nix
new file mode 100644
index 000000000000..1be0b901eb3e
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/default.nix
@@ -0,0 +1,26 @@
+{ pkgs, depot, ... }:
+
+let
+  list = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "list";
+        version = "1.0.0";
+        src = ./list.el;
+        packageRequires =
+          (with depot.users.wpcarro.emacs.pkgs; [
+            maybe
+            set
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [ list ]);
+in
+list.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/list/list.el b/users/wpcarro/emacs/pkgs/list/list.el
new file mode 100644
index 000000000000..18be5f0a716c
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/list.el
@@ -0,0 +1,219 @@
+;;; 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 where I prefer more modern
+;; alternatives:
+;; - `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 influenced by
+;; Elixir's standard library.
+;;
+;; Similar libraries:
+;; - dash.el: Excellent and widely adopted library for working with lists.
+;; - list-utils.el: Utility library that covers things that dash.el may not
+;;   cover.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'maybe)
+(require 'set)
+(require 'set)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun list-new ()
+  "Return a new, empty list."
+  '())
+
+(defun list-concat (&rest lists)
+  "Joins `LISTS' into on list."
+  (apply #'append lists))
+
+(defun list-duplicate (n x)
+  "Duplicates the given element, X, N times in a list."
+  (list-map (lambda (_) x) (number-sequence 1 n)))
+
+(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)
+                   (format "%s%s%s" 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-first (xs &optional default)
+  "Alias for `list-head' for `XS'."
+  (if (list-empty? xs)
+      default
+    (car xs)))
+
+(defun list-last (xs &optional default)
+  "Returns the last element in XS or DEFAULT if empty."
+  (if (list-empty? xs)
+      default
+    (nth (- (length xs) 1) 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))
+
+(defun list-delete (x xs)
+  "Deletes the given element, X, from XS.
+Returns a new list without X. If X occurs more than once, only the first
+  occurrence is removed."
+  (let ((deleted? nil))
+    (list-reject (lambda (y)
+                   (if deleted? nil
+                     (when (equal x y)
+                       (setq deleted? t) t)))
+                 xs)))
+
+(defun list-filter (p xs)
+  "Return a subset of XS where predicate P returned t."
+  (list--assert-instance xs)
+  (seq-filter p xs))
+
+(defun list-map (f xs)
+  "Call `F' on each element of `XS'."
+  (list--assert-instance xs)
+  (seq-map f xs))
+
+(defun list-reduce (acc f xs)
+  "Return over `XS' calling `F' on an element in `XS'and `ACC'."
+  (list--assert-instance xs)
+  (seq-reduce (lambda (acc x) (funcall f x acc)) xs acc))
+
+(defun list-map-indexed (f xs)
+  "Call `F' on each element of `XS' along with its index."
+  (list-reverse
+   (cdr
+    (list-reduce '(0 . nil)
+                 (lambda (x acc)
+                   (let ((i (car acc))
+                         (result (cdr acc)))
+                     `(,(+ 1 i) . ,(cons (funcall f x i) result))))
+                 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."
+  (list--assert-instance xs)
+  (seq-find p xs))
+
+(defun list-dedupe-adjacent (xs)
+  "Return XS without adjacent duplicates."
+  (list-reverse
+   (list-reduce (list (list-first xs))
+                (lambda (x acc)
+                  (if (equal x (list-first acc))
+                      acc
+                    (list-cons x acc)))
+                xs)))
+
+(defun list-chunk (n xs)
+  "Chunk XS into lists of size N."
+  (if (> n (length xs))
+      (list xs)
+    (let* ((xs (list-reduce '(:curr () :result ())
+                            (lambda (x acc)
+                              (let ((curr (plist-get acc :curr))
+                                    (result (plist-get acc :result)))
+                                (if (= (- n 1) (length curr))
+                                    `(:curr () :result ,(list-cons (list-reverse (list-cons x curr)) result))
+                                  `(:curr ,(list-cons x curr) :result
+                                          ,result)))) xs))
+           (curr (plist-get xs :curr))
+           (result (plist-get xs :result)))
+      (list-reverse (if curr (list-cons curr result) result)))))
+
+(defun list-wrap (xs)
+  "Wraps XS in a list if it is not a list already."
+  (if (list-instance? xs)
+      xs
+    (list 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'."
+  (if (list-empty? xs)
+      t
+    (and (maybe-some? (funcall p (car xs)))
+         (list-all? p (cdr xs)))))
+
+(defun list-any? (p xs)
+  "Return t if any `XS' pass the predicate, `P'."
+  (if (list-empty? xs)
+      nil
+    (or (maybe-some? (funcall p (car xs)))
+        (list-any? p (cdr xs)))))
+
+(defun list-contains? (x xs)
+  "Return t if X is in XS using `equal'."
+  (list--assert-instance xs)
+  (maybe-some? (seq-contains-p 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)
+     (set-count (set-from-list (list-map f xs)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helpers
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun list--assert-instance (xs)
+  (unless (list-instance? xs)
+    (error (format "Assertion failed: argument is not a list: %s" xs))))
+
+(provide 'list)
+;;; list.el ends here
diff --git a/users/wpcarro/emacs/pkgs/list/tests.el b/users/wpcarro/emacs/pkgs/list/tests.el
new file mode 100644
index 000000000000..4b4579688331
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/list/tests.el
@@ -0,0 +1,107 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'list)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(setq xs '(1 2 3 4 5))
+
+(ert-deftest list-length ()
+  (should (= 0 (list-length '())))
+  (should (= 5 (list-length xs))))
+
+(ert-deftest list-reduce ()
+  (should (= 16 (list-reduce 1 (lambda (x acc) (+ x acc)) xs))))
+
+(ert-deftest list-map ()
+  (should
+   (equal '(2 4 6 8 10)
+          (list-map (lambda (x) (* x 2)) xs))))
+
+(ert-deftest list-xs-distinct-by? ()
+  (should
+   (equal t (list-xs-distinct-by?
+             (lambda (x) (plist-get x :kbd))
+             '((:kbd "C-a" :name "foo")
+               (:kbd "C-b" :name "foo"))))))
+
+(ert-deftest list-dedupe-adjacent ()
+  (should (equal '(1 2 3 4 3 5)
+                 (list-dedupe-adjacent '(1 1 1 2 2 3 4 4 3 5 5)))))
+
+(ert-deftest list-contains? ()
+  ;; Assert returns t or nil
+  (should (equal t (list-contains? 1 xs)))
+  (should (equal nil (list-contains? 100 xs))))
+
+(ert-deftest list-join ()
+  (should (equal "foo-bar-baz"
+                 (list-join "-" '("foo" "bar" "baz")))))
+
+(ert-deftest list-chunk ()
+  (should (equal '((1 2 3 4 5 6))
+                 (list-chunk 7 '(1 2 3 4 5 6))))
+  (should (equal '((1) (2) (3) (4) (5) (6))
+                 (list-chunk 1 '(1 2 3 4 5 6))))
+  (should (equal '((1 2 3) (4 5 6))
+                 (list-chunk 3 '(1 2 3 4 5 6))))
+  (should (equal '((1 2) (3 4) (5 6))
+                 (list-chunk 2 '(1 2 3 4 5 6)))))
+
+(ert-deftest list-find ()
+  (should (equal 2 (list-find (lambda (x) (= 2 x)) '(1 2 3 4)))))
+
+(ert-deftest list-all? ()
+  (should (equal t (list-all? (lambda (x) (= 2 x)) nil)))
+  (should (null (list-all? (lambda (x) (= 2 x)) '(1 2 3))))
+  (should (equal t (list-all? (lambda (x) (= 2 x)) '(2 2 2 2)))))
+
+(ert-deftest list-any? ()
+  (should (null (list-any? (lambda (x) (= 2 x)) nil)))
+  (should (equal t (list-any? (lambda (x) (= 2 x)) '(1 2 3))))
+  (should (null (list-any? (lambda (x) (= 4 x)) '(1 2 3)))))
+
+(ert-deftest list-duplicate ()
+  (should (equal '() (list-duplicate 0 "hello")))
+  (should (equal '("hi") (list-duplicate 1 "hi")))
+  (should (equal '("bye" "bye") (list-duplicate 2 "bye")))
+  (should (equal '((1 2) (1 2) (1 2)) (list-duplicate 3 '(1 2)))))
+
+(ert-deftest list-first ()
+  (should (null (list-first '())))
+  (should (equal 1 (list-first '() 1)))
+  (should (equal 1 (list-first '(1))))
+  (should (equal 1 (list-first '(1) 2)))
+  (should (equal 1 (list-first '(1 2 3)))))
+
+(ert-deftest list-last ()
+  (should (null (list-last '())))
+  (should (equal 1 (list-last '() 1)))
+  (should (equal 1 (list-last '(1))))
+  (should (equal 1 (list-last '(1) 2)))
+  (should (equal 3 (list-last '(1 2 3)))))
+
+(ert-deftest list-wrap ()
+  (should (equal '("hello") (list-wrap "hello")))
+  (should (equal '(1 2 3) (list-wrap '(1 2 3))))
+  (should (equal '() (list-wrap nil))))
+
+(ert-deftest list-delete ()
+  (should (equal '(b c) (list-delete 'a '(a b c))))
+  (should (equal '(a b c) (list-delete 'd '(a b c))))
+  (should (equal '(a b c) (list-delete 'b '(a b b c))))
+  (should (equal '() (list-delete 'b '()))))
+
+(ert-deftest list-concat ()
+  (should (equal '(1 2 3 4 5) (list-concat '(1) '(2 3) '(4 5))))
+  (should (equal '(1 2 3) (list-concat '() '(1 2 3)))))
+
+;; TODO(wpcarro): Supoprt this.
+;; (ert-deftest list-zip ()
+;;   (should (equal '((1 3 5) (2 4 6)) (list-zip '(1 2) '(3 4) '(5 6))))
+;;   (should (equal '((1 3 5)) (list-zip '(1 2) '(3) '(5 6)))))
diff --git a/users/wpcarro/emacs/pkgs/macros/default.nix b/users/wpcarro/emacs/pkgs/macros/default.nix
new file mode 100644
index 000000000000..d2811ed39fc6
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/macros/default.nix
@@ -0,0 +1,10 @@
+{ pkgs, depot, ... }:
+
+pkgs.callPackage
+  ({ emacsPackages }:
+  emacsPackages.trivialBuild {
+    pname = "macros";
+    version = "1.0.0";
+    src = ./macros.el;
+  })
+{ }
diff --git a/users/wpcarro/emacs/pkgs/macros/macros.el b/users/wpcarro/emacs/pkgs/macros/macros.el
new file mode 100644
index 000000000000..3642686eeb4b
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/macros/macros.el
@@ -0,0 +1,45 @@
+;;; 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.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; 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))))
+
+(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 (format "\\.%s\\'" ext)))
+    `(add-to-list 'auto-mode-alist '(,extension . ,mode))))
+
+(provide 'macros)
+;;; macros.el ends here
diff --git a/users/wpcarro/emacs/pkgs/math/default.nix b/users/wpcarro/emacs/pkgs/math/default.nix
new file mode 100644
index 000000000000..9167d61d4edc
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/math/default.nix
@@ -0,0 +1,30 @@
+{ pkgs, depot, ... }:
+
+let
+  math = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "math";
+        version = "1.0.0";
+        src = ./math.el;
+        packageRequires =
+          (with emacsPackages; [
+            dash
+          ]) ++
+          (with depot.users.wpcarro.emacs.pkgs; [
+            maybe
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    math
+  ]);
+in
+math.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/math/math.el b/users/wpcarro/emacs/pkgs/math/math.el
new file mode 100644
index 000000000000..dbc527928a30
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/math/math.el
@@ -0,0 +1,63 @@
+;;; 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 'dash)
+(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
+   ((-all? #'maybe-some? (list base power result))
+    (error "All three arguments should not be set"))
+   ((-all? #'maybe-some? (list power result))
+    (message "power and result"))
+   ((-all? #'maybe-some? (list base result))
+    (log result base))
+   ((-all? #'maybe-some? (list 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/pkgs/math/tests.el b/users/wpcarro/emacs/pkgs/math/tests.el
new file mode 100644
index 000000000000..ef3430c9131b
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/math/tests.el
@@ -0,0 +1,25 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'math)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest math-mod ()
+  (should (= 0 (math-mod 9 3)))
+  (should (= 4 (math-mod 9 5))))
+
+(ert-deftest math-exp ()
+  (should (= 9 (math-exp 3 2)))
+  (should (= 8 (math-exp 2 3))))
+
+(ert-deftest math-round ()
+  (should (= 10 (math-round 9.5)))
+  (should (= 9 (math-round 9.45))))
+
+(ert-deftest math-floor ()
+  (should (= 9 (math-floor 9.5))))
diff --git a/users/wpcarro/emacs/pkgs/maybe/default.nix b/users/wpcarro/emacs/pkgs/maybe/default.nix
new file mode 100644
index 000000000000..68e058b42b19
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/maybe/default.nix
@@ -0,0 +1,24 @@
+{ pkgs, depot, ... }:
+
+let
+  maybe = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "maybe";
+        version = "1.0.0";
+        src = ./maybe.el;
+        packageRequires = [ ];
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    maybe
+  ]);
+in
+maybe.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/maybe/maybe.el b/users/wpcarro/emacs/pkgs/maybe/maybe.el
new file mode 100644
index 000000000000..581568d8ccba
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/maybe/maybe.el
@@ -0,0 +1,54 @@
+;;; 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.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun maybe-nil? (x)
+  "Return t if X is nil."
+  (null x))
+
+(defun maybe-some? (x)
+  "Return t when X is non-nil."
+  (not (maybe-nil? x)))
+
+(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/pkgs/maybe/tests.el b/users/wpcarro/emacs/pkgs/maybe/tests.el
new file mode 100644
index 000000000000..c0463cc65a53
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/maybe/tests.el
@@ -0,0 +1,25 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'maybe)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest maybe-nil? ()
+  (should (maybe-nil? nil))
+  (should (not (maybe-nil? t))))
+
+(ert-deftest maybe-some? ()
+  (should (maybe-some? '(1 2 3)))
+  (should (not (maybe-some? nil))))
+
+(ert-deftest maybe-default ()
+  (should (string= "some" (maybe-default "some" nil)))
+  (should (= 10 (maybe-default 1 10))))
+
+(ert-deftest maybe-map ()
+  (should (eq nil (maybe-map (lambda (x) (* x 2)) nil)))
+  (should (= 4 (maybe-map (lambda (x) (* x 2)) 2))))
diff --git a/users/wpcarro/emacs/pkgs/passage/README.md b/users/wpcarro/emacs/pkgs/passage/README.md
new file mode 100644
index 000000000000..51f7bd6efda4
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/passage/README.md
@@ -0,0 +1,12 @@
+# passage.el
+
+Emacs support for `passage`.
+
+## Alternative Packages
+
+If you're looking for more feature-complete, configurable alternatives,
+check-out the following packages:
+
+- `ivy-pass.el`
+- `password-store.el`
+- `pass.el`
diff --git a/users/wpcarro/emacs/pkgs/passage/default.nix b/users/wpcarro/emacs/pkgs/passage/default.nix
new file mode 100644
index 000000000000..ac87f193b4e2
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/passage/default.nix
@@ -0,0 +1,12 @@
+{ pkgs, depot, ... }:
+
+pkgs.callPackage
+  ({ emacsPackages }:
+  emacsPackages.trivialBuild {
+    pname = "passage";
+    version = "1.0.0";
+    src = ./passage.el;
+    packageRequires = (with emacsPackages; [ dash f s ]);
+  }
+  )
+{ }
diff --git a/users/wpcarro/emacs/pkgs/passage/passage.el b/users/wpcarro/emacs/pkgs/passage/passage.el
new file mode 100644
index 000000000000..4a43920e0bed
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/passage/passage.el
@@ -0,0 +1,65 @@
+;;; passage.el --- Emacs passage support -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2023 William Carroll <wpcarro@gmail.com>
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 1.0.0
+
+;; This file is not part of GNU Emacs.
+
+;; 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/>.
+
+;;; Commentary:
+
+;; This package provides functions for working with passage.
+
+;;; Code:
+
+(require 'dash)
+(require 'f)
+(require 's)
+
+(defgroup passage nil
+  "Customization options for `passage'."
+  :prefix "passage-"
+  :group 'vterm)
+
+(defcustom passage-store
+  "~/.passage/store"
+  "Path to the passage store directory."
+  :type 'string
+  :group 'passage)
+
+(defcustom passage-executable
+  (or (executable-find "passage")
+      "/nix/store/jgffkfdiiwiqa4zqpxn3691mx9xc6axa-passage-unstable-2022-05-01/bin/passage")
+  "Path to passage executable."
+  :type 'string
+  :group 'passage)
+
+(defun passage-select ()
+  "Select an entry and copy its password to the kill ring."
+  (interactive)
+  (let ((key (completing-read "Copy password of entry: "
+                              (-map (lambda (x)
+                                      (f-no-ext (f-relative x passage-store)))
+                                    (f-files passage-store nil t)))))
+    (kill-new
+     (s-trim-right
+      (shell-command-to-string
+       (format "%s show %s | head -1" passage-executable key))))
+    (message "[passage.el] Copied \"%s\"!" key)))
+
+(provide 'passage)
+;;; passage.el ends here
diff --git a/users/wpcarro/emacs/pkgs/set/default.nix b/users/wpcarro/emacs/pkgs/set/default.nix
new file mode 100644
index 000000000000..319ba9274423
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/set/default.nix
@@ -0,0 +1,32 @@
+{ pkgs, depot, ... }:
+
+let
+  set = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "set";
+        version = "1.0.0";
+        src = ./set.el;
+        packageRequires =
+          (with emacsPackages; [
+            dash
+            ht
+          ]) ++
+          (with depot.users.wpcarro.emacs.pkgs; [
+            struct
+          ]);
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    epkgs.dash
+    set
+  ]);
+in
+set.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/set/set.el b/users/wpcarro/emacs/pkgs/set/set.el
new file mode 100644
index 000000000000..2d6e14917a45
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/set/set.el
@@ -0,0 +1,116 @@
+;;; 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:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'cl-lib)
+(require 'dash)
+(require 'ht) ;; friendlier API for hash-tables
+(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)
+
+(defun set-from-list (xs)
+  "Create a new set from the list XS."
+  (make-set :xs (->> xs
+                     (-map (lambda (x) (cons x nil)))
+                     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
+       (-reduce-from (lambda (acc x) (funcall f x acc)) acc)))
+
+(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
+       (-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))
+
+(provide 'set)
+;;; set.el ends here
diff --git a/users/wpcarro/emacs/pkgs/set/tests.el b/users/wpcarro/emacs/pkgs/set/tests.el
new file mode 100644
index 000000000000..7f5c2ae3ffd9
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/set/tests.el
@@ -0,0 +1,69 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'dash)
+(require 'set)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest set-from-list ()
+  (should (equal '(1 2 3)
+                 (->> '(1 2 3 1 2 3)
+                      set-from-list
+                      set-to-list))))
+
+(ert-deftest set-distinct? ()
+  (should (set-distinct? (set-new 'one 'two 'three)
+                         (set-new 'a 'b 'c)))
+  (should (not
+           (set-distinct? (set-new 1 2 3)
+                          (set-new 3 4 5))))
+  (should (not
+           (set-distinct? (set-new 1 2 3)
+                          (set-new 1 2 3)))))
+
+(ert-deftest set-equal? ()
+  (should (not (set-equal? (set-new 'a 'b 'c)
+                           (set-new 'x 'y 'z))))
+  (should (not (set-equal? (set-new 'a 'b 'c)
+                           (set-new 'a 'b))))
+  (should (set-equal? (set-new 'a 'b 'c)
+                      (set-new 'a 'b 'c))))
+
+(ert-deftest set-intersection ()
+  (should (set-equal? (set-new 2 3)
+                      (set-intersection (set-new 1 2 3)
+                                        (set-new 2 3 4)))))
+
+(ert-deftest set-to/from-list ()
+  (should (equal '(1 2 3)
+                 (->> '(1 1 2 2 3 3)
+                      set-from-list
+                      set-to-list))))
+
+(ert-deftest set-subset? ()
+  (should (not (set-subset? (set-new "black" "grey")
+                            (set-new "red" "green" "blue"))))
+  (should (set-subset? (set-new "red")
+                       (set-new "red" "green" "blue"))))
+
+(ert-deftest set-superset? ()
+  (let ((primary-colors (set-new "red" "green" "blue")))
+    (should (not (set-superset? primary-colors
+                                (set-new "black" "grey"))))
+    (should (set-superset? primary-colors
+                           (set-new "red" "green" "blue")))
+    (should (set-superset? primary-colors
+                           (set-new "red" "blue")))))
+
+(ert-deftest set-empty? ()
+  (should (set-empty? (set-new)))
+  (should (not (set-empty? (set-new 1 2 3)))))
+
+(ert-deftest set-count ()
+  (should (= 0 (set-count (set-new))))
+  (should (= 2 (set-count (set-new 1 1 2 2)))))
diff --git a/users/wpcarro/emacs/pkgs/string/default.nix b/users/wpcarro/emacs/pkgs/string/default.nix
new file mode 100644
index 000000000000..406cccdfcb58
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/string/default.nix
@@ -0,0 +1,27 @@
+{ pkgs, depot, ... }:
+
+let
+  string = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "string";
+        version = "1.0.0";
+        src = ./string.el;
+        packageRequires = [
+          emacsPackages.dash
+          emacsPackages.s
+        ];
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    string
+  ]);
+in
+string.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/string/string.el b/users/wpcarro/emacs/pkgs/string/string.el
new file mode 100644
index 000000000000..30da1805e83f
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/string/string.el
@@ -0,0 +1,98 @@
+;;; 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 strings.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 's)
+(require 'dash)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun string-split (y x)
+  "Map string X into a list of strings that were separated by Y."
+  (s-split y 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-to-symbol (string)
+  "Maps `STRING' to a symbol."
+  (intern string))
+
+(defun string-from-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))
+
+(defun string-contains? (c x)
+  "Return t if X is in C."
+  (s-contains? c x))
+
+(provide 'string)
+;;; string.el ends here
diff --git a/users/wpcarro/emacs/pkgs/string/tests.el b/users/wpcarro/emacs/pkgs/string/tests.el
new file mode 100644
index 000000000000..351e305466d3
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/string/tests.el
@@ -0,0 +1,22 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'string)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest string-caps->kebab ()
+  (should (string= "foo-bar-baz" (string-caps->kebab "FOO_BAR_BAZ"))))
+
+(ert-deftest string-kebab->caps ()
+  (should (string= "FOO_BAR_BAZ" (string-kebab->caps "foo-bar-baz"))))
+
+(ert-deftest string-lower->caps ()
+  (should (string= "FOO_BAR_BAZ" (string-lower->caps "foo bar baz"))))
+
+(ert-deftest string-lower->kebab ()
+  (should (string= "foo-bar-baz" (string-lower->kebab "foo bar baz"))))
diff --git a/users/wpcarro/emacs/pkgs/struct/README.md b/users/wpcarro/emacs/pkgs/struct/README.md
new file mode 100644
index 000000000000..34dac6614cd1
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/struct/README.md
@@ -0,0 +1,6 @@
+# struct.el
+
+[![Build status](https://badge.buildkite.com/016bff4b8ae2704a3bbbb0a250784e6692007c582983b6dea7.svg?branch=refs/heads/canon)](https://buildkite.com/tvl/depot)
+
+Provides new macros exposing immutable and mutable interfaces for working with
+structs in Elisp.
diff --git a/users/wpcarro/emacs/pkgs/struct/default.nix b/users/wpcarro/emacs/pkgs/struct/default.nix
new file mode 100644
index 000000000000..558ebd0a3d20
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/struct/default.nix
@@ -0,0 +1,29 @@
+{ pkgs, depot, ... }:
+
+let
+  struct = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "struct";
+        version = "1.0.0";
+        src = ./struct.el;
+        packageRequires = [ ];
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    struct
+  ]);
+in
+struct.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+  passthru.meta.ci.extraSteps.github = depot.tools.releases.filteredGitPush {
+    filter = ":/users/wpcarro/emacs/pkgs/struct";
+    remote = "git@github.com:wpcarro/struct.el.git";
+    ref = "refs/heads/canon";
+  };
+})
diff --git a/users/wpcarro/emacs/pkgs/struct/struct.el b/users/wpcarro/emacs/pkgs/struct/struct.el
new file mode 100644
index 000000000000..5d6572bf6dde
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/struct/struct.el
@@ -0,0 +1,65 @@
+;;; struct.el --- Helpers for working with structs -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 1.0.0
+;; 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:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defmacro struct-update (type field f xs)
+  "Apply F to FIELD in XS, which is a struct of TYPE.
+This is immutable."
+  (let ((copier (struct--copier-for type))
+        (accessor (struct--accessor-for type field)))
+    `(let ((copy (,copier ,xs)))
+       (setf (,accessor copy) (funcall ,f (,accessor copy)))
+       copy)))
+
+(defmacro struct-update! (type field f xs)
+  "Mutably apply F to FIELD in XS."
+  (let ((accessor (struct--accessor-for type field)))
+    `(progn
+       (setf (,accessor ,xs) (funcall ,f (,accessor ,xs)))
+       ,xs)))
+
+(defmacro struct-set (type field x xs)
+  "Immutably set FIELD in XS (struct TYPE) to X."
+  (let ((copier (struct--copier-for type))
+        (accessor (struct--accessor-for type field)))
+    `(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 (struct--accessor-for type field)))
+    `(progn
+       (setf (,accessor ,xs) ,x)
+       ,xs)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Helper Functions
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun struct--copier-for (type)
+  (intern (format "copy-%s" (symbol-name type))))
+
+(defun struct--accessor-for (type field)
+  (intern (format "%s-%s"
+                  (symbol-name type)
+                  (symbol-name field))))
+
+(provide 'struct)
+;;; struct.el ends here
diff --git a/users/wpcarro/emacs/pkgs/struct/tests.el b/users/wpcarro/emacs/pkgs/struct/tests.el
new file mode 100644
index 000000000000..a7ddb52c46d6
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/struct/tests.el
@@ -0,0 +1,44 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'struct)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(cl-defstruct dummy name age)
+
+(ert-deftest struct-update ()
+  (let* ((test (make-dummy :name "Roofus" :age 19))
+         (result (struct-update dummy name #'upcase test)))
+    ;; test
+    (should (string= "Roofus" (dummy-name test)))
+    (should (= 19 (dummy-age test)))
+    ;; result
+    (should (string= "ROOFUS" (dummy-name result)))
+    (should (= 19 (dummy-age result)))))
+
+(ert-deftest struct-update! ()
+  (let ((test (make-dummy :name "Roofus" :age 19)))
+    (struct-update! dummy name #'upcase test)
+    (should (string= "ROOFUS" (dummy-name test)))
+    (should (= 19 (dummy-age test)))))
+
+(ert-deftest struct-set ()
+  (let* ((test (make-dummy :name "Roofus" :age 19))
+         (result (struct-set dummy name "Shoofus" test)))
+    ;; test
+    (should (string= "Roofus" (dummy-name test)))
+    (should (= 19 (dummy-age test)))
+    ;; result
+    (should (string= "Shoofus" (dummy-name result)))
+    (should (= 19 (dummy-age result)))))
+
+(ert-deftest struct-set! ()
+  (let ((test (make-dummy :name "Roofus" :age 19)))
+    (struct-set! dummy name "Doofus" test)
+    (should (string= "Doofus" (dummy-name test)))
+    (should (= 19 (dummy-age test)))))
diff --git a/users/wpcarro/emacs/pkgs/symbol/default.nix b/users/wpcarro/emacs/pkgs/symbol/default.nix
new file mode 100644
index 000000000000..9334697e3203
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/symbol/default.nix
@@ -0,0 +1,24 @@
+{ pkgs, depot, ... }:
+
+let
+  symbol = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "symbol";
+        version = "1.0.0";
+        src = ./symbol.el;
+        packageRequires = [ ];
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [
+    symbol
+  ]);
+in
+symbol.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/symbol/symbol.el b/users/wpcarro/emacs/pkgs/symbol/symbol.el
new file mode 100644
index 000000000000..4b16351831c9
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/symbol/symbol.el
@@ -0,0 +1,38 @@
+;;; 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:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun symbol-to-string (symbol)
+  "Map `SYMBOL' into a string."
+  (symbol-name symbol))
+
+(defun symbol-from-string (string)
+  "Map `STRING' into a symbol."
+  (intern string))
+
+(defun symbol-as-string (f x)
+  "Treat the symbol, X, as a string while applying F to it.
+Coerce back to a symbol on the way out."
+  (symbol-from-string (funcall f (symbol-to-string x))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predicates
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(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/pkgs/symbol/tests.el b/users/wpcarro/emacs/pkgs/symbol/tests.el
new file mode 100644
index 000000000000..b10362b162c7
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/symbol/tests.el
@@ -0,0 +1,22 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'symbol)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest symbol-to-string ()
+  (should (string= "foo" (symbol-to-string 'foo))))
+
+(ert-deftest symbol-from-string ()
+  (should (eq 'foo (symbol-from-string "foo"))))
+
+(ert-deftest symbol-as-string ()
+  (should (eq 'foo-hook
+              (symbol-as-string
+               (lambda (x) (format "%s-hook" x))
+               'foo))))
diff --git a/users/wpcarro/emacs/pkgs/theme/default.nix b/users/wpcarro/emacs/pkgs/theme/default.nix
new file mode 100644
index 000000000000..aea639436961
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/theme/default.nix
@@ -0,0 +1,14 @@
+{ pkgs, depot, ... }:
+
+pkgs.callPackage
+  ({ emacsPackages }:
+  emacsPackages.trivialBuild {
+    pname = "theme";
+    version = "1.0.0";
+    src = ./theme.el;
+    packageRequires =
+      (with depot.users.wpcarro.emacs.pkgs; [
+        cycle
+      ]);
+  })
+{ }
diff --git a/users/wpcarro/emacs/pkgs/theme/theme.el b/users/wpcarro/emacs/pkgs/theme/theme.el
new file mode 100644
index 000000000000..32f2c89a4d0b
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/theme/theme.el
@@ -0,0 +1,78 @@
+;;; theme.el --- Colors and stuff -*- lexical-binding: t -*-
+
+;; Author: William Carroll <wpcarro@gmail.com>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "24.3"))
+
+;;; Commentary:
+;;
+;; Cycle through a whitelist of themes.
+
+;;; Code:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'cycle)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgroup theme nil
+  "Customization options for `theme'."
+  :group 'theme)
+
+(defcustom theme-whitelist
+  (cycle-from-list (custom-available-themes))
+  "The whitelist of themes through which to cycle."
+  :type '(cycle symbol)
+  :group 'theme)
+
+(defcustom theme-after-change
+  nil
+  "Hook invoked after a new theme is loaded"
+  :type 'hook
+  :group 'theme)
+
+(defun theme-whitelist-set (theme)
+  "Focus the THEME in the `theme-whitelist' cycle."
+  (cycle-focus! (lambda (x) (equal x theme)) theme-whitelist)
+  (theme--set (cycle-current theme-whitelist)))
+
+(defun theme-select ()
+  "Load a theme using `completing-read'."
+  (interactive)
+  (let ((theme (completing-read "Theme: " (cycle-to-list theme-whitelist))))
+    (theme--disable-all)
+    (theme--set (intern theme))))
+
+(defun theme-next ()
+  "Disable the currently active theme and load the next theme."
+  (interactive)
+  (disable-theme (cycle-current theme-whitelist))
+  (theme--set (cycle-next! theme-whitelist))
+  (message (format "Active theme: %s" (cycle-current theme-whitelist))))
+
+(defun theme-prev ()
+  "Disable the currently active theme and load the previous theme."
+  (interactive)
+  (disable-theme (cycle-current theme-whitelist))
+  (theme--set (cycle-prev! theme-whitelist))
+  (message (format "Active theme: %s" (cycle-current theme-whitelist))))
+
+(defun theme--disable-all ()
+  "Disable all currently enabled themes."
+  (interactive)
+  (dolist (x custom-enabled-themes)
+    (disable-theme x)))
+
+(defun theme--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)
+    (run-hooks 'theme-after-change))
+
+(provide 'theme)
+;;; theme.el ends here
diff --git a/users/wpcarro/emacs/pkgs/tuple/default.nix b/users/wpcarro/emacs/pkgs/tuple/default.nix
new file mode 100644
index 000000000000..0626370e4795
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/tuple/default.nix
@@ -0,0 +1,10 @@
+{ pkgs, depot, ... }:
+
+pkgs.callPackage
+  ({ emacsPackages }:
+  emacsPackages.trivialBuild {
+    pname = "tuple";
+    version = "1.0.0";
+    src = ./tuple.el;
+  })
+{ }
diff --git a/users/wpcarro/emacs/pkgs/tuple/tuple.el b/users/wpcarro/emacs/pkgs/tuple/tuple.el
new file mode 100644
index 000000000000..848c6fa48b15
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/tuple/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/pkgs/vector/default.nix b/users/wpcarro/emacs/pkgs/vector/default.nix
new file mode 100644
index 000000000000..c0a475aaaa82
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/vector/default.nix
@@ -0,0 +1,21 @@
+{ pkgs, depot, ... }:
+
+let
+  vector = pkgs.callPackage
+    ({ emacsPackages }:
+      emacsPackages.trivialBuild {
+        pname = "vector";
+        version = "1.0.0";
+        src = ./vector.el;
+      })
+    { };
+
+  emacs = (pkgs.emacsPackagesFor pkgs.emacs28).emacsWithPackages (epkgs: [ vector ]);
+in
+vector.overrideAttrs (_old: {
+  doCheck = true;
+  checkPhase = ''
+    ${emacs}/bin/emacs -batch \
+      -l ert -l ${./tests.el} -f ert-run-tests-batch-and-exit
+  '';
+})
diff --git a/users/wpcarro/emacs/pkgs/vector/tests.el b/users/wpcarro/emacs/pkgs/vector/tests.el
new file mode 100644
index 000000000000..ffa983188229
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/vector/tests.el
@@ -0,0 +1,20 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'ert)
+(require 'vector)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Tests
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(ert-deftest vector-misc-tests ()
+  (let ((xs [1 2 3])
+        (ys [1 2 3]))
+    (should (= 1 (vector-get 0 ys)))
+    (vector-set 0 4 ys)
+    (should (= 1 (vector-get 0 ys)))
+    (should (= 1 (vector-get 0 xs)))
+    (vector-set! 0 4 xs)
+    (should (= 4 (vector-get 0 xs)))))
diff --git a/users/wpcarro/emacs/pkgs/vector/vector.el b/users/wpcarro/emacs/pkgs/vector/vector.el
new file mode 100644
index 000000000000..87f38d7d93e2
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/vector/vector.el
@@ -0,0 +1,58 @@
+;;; 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:
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Library
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun vector-concat (&rest args)
+  "Return a new vector composed of all vectors in `ARGS'."
+  (apply #'vconcat args))
+
+(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))
+
+(provide 'vector)
+;;; vector.el ends here
diff --git a/users/wpcarro/emacs/pkgs/vterm-mgt/README.md b/users/wpcarro/emacs/pkgs/vterm-mgt/README.md
new file mode 100644
index 000000000000..b855826929ca
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/vterm-mgt/README.md
@@ -0,0 +1,17 @@
+# vterm-mgt.el
+
+[![Build status](https://badge.buildkite.com/016bff4b8ae2704a3bbbb0a250784e6692007c582983b6dea7.svg?branch=refs/heads/canon)](https://buildkite.com/tvl/depot)
+
+[emacs-libvterm](https://github.com/akermu/emacs-libvterm) is a feature-complete
+terminal emulator inside Emacs based on libvterm.
+
+`vterm-mgt.el`, adds functionality on top of `vterm` to allow you to:
+
+* find-or-create `vterm` instances
+* fuzzily switch between existing `vterm` buffers
+* cycle through existing `vterm` instances
+* easily rename `vterm` buffers
+
+## Alternatives to vterm-mgt.el
+
+* [multi-vterm](https://github.com/suonlight/multi-vterm)
diff --git a/users/wpcarro/emacs/pkgs/vterm-mgt/default.nix b/users/wpcarro/emacs/pkgs/vterm-mgt/default.nix
new file mode 100644
index 000000000000..88eb5502042a
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/vterm-mgt/default.nix
@@ -0,0 +1,19 @@
+{ pkgs, depot, ... }:
+
+pkgs.emacsPackages.trivialBuild {
+  pname = "vterm-mgt";
+  version = "1.0.0";
+  src = ./vterm-mgt.el;
+  packageRequires =
+    (with pkgs.emacsPackages; [
+      vterm
+    ]) ++
+    (with depot.users.wpcarro.emacs.pkgs; [
+      cycle
+    ]);
+  passthru.meta.ci.extraSteps.github = depot.tools.releases.filteredGitPush {
+    filter = ":/users/wpcarro/emacs/pkgs/vterm-mgt";
+    remote = "git@github.com:wpcarro/vterm-mgt.el.git";
+    ref = "refs/heads/canon";
+  };
+}
diff --git a/users/wpcarro/emacs/pkgs/vterm-mgt/vterm-mgt.el b/users/wpcarro/emacs/pkgs/vterm-mgt/vterm-mgt.el
new file mode 100644
index 000000000000..c082e54a5976
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/vterm-mgt/vterm-mgt.el
@@ -0,0 +1,140 @@
+;;; 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 'cycle)
+(require 'vterm)
+(require 'seq)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defgroup vterm-mgt nil
+  "Customization options for `vterm-mgt'."
+  :group 'vterm)
+
+(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)))
+
+(defun vterm-mgt--assert-vterm-buffer ()
+  "Error when the `current-buffer' is not a vterm buffer."
+  (unless (vterm-mgt--instance? (current-buffer))
+    (error "Current buffer is not a vterm 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
+        (cycle-from-list (seq-filter #'vterm-mgt--instance? (buffer-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: "
+                    (seq-map #'buffer-name (cycle-to-list vterm-mgt--instances)))))
+
+(provide 'vterm-mgt)
+;;; vterm-mgt.el ends here
diff --git a/users/wpcarro/emacs/pkgs/zle/default.nix b/users/wpcarro/emacs/pkgs/zle/default.nix
new file mode 100644
index 000000000000..9d4820a9445e
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/zle/default.nix
@@ -0,0 +1,10 @@
+{ pkgs, ... }:
+
+pkgs.callPackage
+  ({ emacsPackages }:
+  emacsPackages.trivialBuild {
+    pname = "zle";
+    version = "1.0.0";
+    src = ./zle.el;
+  })
+{ }
diff --git a/users/wpcarro/emacs/pkgs/zle/zle.el b/users/wpcarro/emacs/pkgs/zle/zle.el
new file mode 100644
index 000000000000..21a6e35f13d3
--- /dev/null
+++ b/users/wpcarro/emacs/pkgs/zle/zle.el
@@ -0,0 +1,90 @@
+;;; 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)))
+    (define-key map (kbd "C-j") #'zle-subshell)
+    (define-key map (kbd "C-v") #'zle-variable)
+    (define-key map (kbd "C-M--") #'zle-dash-dash)
+    (define-key map (kbd "M-'") #'zle-single-quote)
+    (define-key map (kbd "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/snippets.md b/users/wpcarro/emacs/snippets.md
new file mode 100644
index 000000000000..2081b5617184
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..1409db185e84
--- /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 000000000000..6cbcd2ee4eaf
--- /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 000000000000..cba8abfc9621
--- /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 000000000000..5cea20754bed
--- /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 000000000000..f777c13fefae
--- /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 000000000000..816321b8770f
--- /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 000000000000..72aae7827ba1
--- /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 000000000000..040cc63e0e7c
--- /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 000000000000..25321f50a042
--- /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 000000000000..7d662d08668b
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..3f3ac1474b49
--- /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 000000000000..6dd43a96291f
--- /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 000000000000..295575f3f48d
--- /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 000000000000..0c6a298bf2b0
--- /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 000000000000..e3967b77de1f
--- /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 000000000000..531d110f715a
--- /dev/null
+++ b/users/wpcarro/keys.nix
@@ -0,0 +1,20 @@
+# wpcarro's public SSH keys
+{ ... }:
+
+rec {
+  ava = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB/5Fuo7wi8rNXVXgNaCK2X6ePCh9LQs/9h7Tj6UeXrl wpcarro@ava";
+  iphone = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEU1tsRQ/cMxi9Hd7Xo+YpiWB5i6qx24EJLCEFBK4q4W wpcarro@iphone";
+  kyoko = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBFILKdkNqfTP5WeoQAV6K3MdTzsDW65ToXGc6KlQ9yl wpcarro@kyoko";
+  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
+    iphone
+    kyoko
+    marcus
+    nathan
+    tarasco
+  ];
+}
diff --git a/users/wpcarro/lib/default.nix b/users/wpcarro/lib/default.nix
new file mode 100644
index 000000000000..6354877dd468
--- /dev/null
+++ b/users/wpcarro/lib/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+{
+  usermod = name: depot.path.origSrc + ("/users/wpcarro/nixos/modules/${name}");
+}
diff --git a/users/wpcarro/lisp/README.md b/users/wpcarro/lisp/README.md
new file mode 100644
index 000000000000..9f8693fa6a10
--- /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 000000000000..3522567ea0f7
--- /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 000000000000..5fe5d628e099
--- /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 000000000000..b0b13746b048
--- /dev/null
+++ b/users/wpcarro/nixos/ava/ava.el
@@ -0,0 +1,61 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'bookmark)
+(require 'display)
+(require 'window-manager)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(bookmark-install-kbd
+ (make-bookmark :label "hadrian"
+                :path "/hadrian"
+                :kbd "h"))
+
+(setq initial-buffer-choice "/hadrian")
+
+(add-to-list 'ssh-hosts "wpcarro@tarasco")
+
+(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)))
+
+;; I *think* this needs to be the last statement in this file.
+(window-manager-init :init-hook #'display-arrange-main)
diff --git a/users/wpcarro/nixos/ava/default.nix b/users/wpcarro/nixos/ava/default.nix
new file mode 100644
index 000000000000..a123f454ca29
--- /dev/null
+++ b/users/wpcarro/nixos/ava/default.nix
@@ -0,0 +1,150 @@
+{ depot, pkgs, lib, ... }:
+{ ... }:
+
+let
+  inherit (depot.users) wpcarro;
+  inherit (depot.users.wpcarro.lib) usermod;
+
+  wpcarrosEmacs = wpcarro.emacs.nixos {
+    load = [ ./ava.el ];
+  };
+
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+in
+{
+  imports = [
+    (usermod "hardware/nopn.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;
+  # Additionall exit node settings that Tailscale recommends.
+  networking.firewall.checkReversePath = "loose";
+
+  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;
+
+    printing = {
+      enable = true;
+      drivers = with pkgs; [ gutenprint ];
+    };
+
+    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 = with wpcarro.keys; [
+    iphone
+    nathan
+    tarasco
+  ];
+  users.users.wpcarro = {
+    initialPassword = "password";
+    isNormalUser = true;
+    extraGroups = [
+      "networkmanager"
+      "wheel"
+      "docker"
+    ];
+    shell = pkgs.fish;
+    openssh.authorizedKeys.keys = with wpcarro.keys; [
+      iphone
+      nathan
+      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
+      ec2-api-tools
+      firefox
+      google-chrome
+      httpie
+      pavucontrol
+      quasselClient
+      remmina
+      tdesktop
+      wpcarrosEmacs
+      xsecurelock
+    ]);
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/wpcarro/nixos/default.nix b/users/wpcarro/nixos/default.nix
new file mode 100644
index 000000000000..1b9432be0f18
--- /dev/null
+++ b/users/wpcarro/nixos/default.nix
@@ -0,0 +1,24 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.users.wpcarro.nixos)
+    ava
+    kyoko
+    marcus
+    tarasco;
+
+  systemFor = sys: (depot.ops.nixos.nixosFor sys).system;
+in
+{
+  avaSystem = systemFor ava;
+  kyokoSystem = systemFor kyoko;
+  marcusSystem = systemFor marcus;
+  tarascoSystem = systemFor ava;
+
+  meta.ci.targets = [
+    "avaSystem"
+    "kyokoSystem"
+    "marcusSystem"
+    "tarascoSystem"
+  ];
+}
diff --git a/users/wpcarro/nixos/iso.nix b/users/wpcarro/nixos/iso.nix
new file mode 100644
index 000000000000..8102c98fb893
--- /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/kyoko/default.nix b/users/wpcarro/nixos/kyoko/default.nix
new file mode 100644
index 000000000000..4bfa29cb8e91
--- /dev/null
+++ b/users/wpcarro/nixos/kyoko/default.nix
@@ -0,0 +1,153 @@
+{ depot, pkgs, lib, ... }:
+_:
+
+let
+  inherit (depot.users) wpcarro;
+  inherit (depot.users.wpcarro.lib) usermod;
+
+  wpcarrosEmacs = wpcarro.emacs.nixos {
+    load = [ ./kyoko.el ];
+  };
+
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+in
+{
+  imports = [
+    (usermod "hardware/dell-emc-egw-5200.nix")
+    (usermod "hadrian-cache.nix")
+  ];
+
+  # TVL's Nix binary cache
+  tvl.cache.enable = true;
+
+  # Hadrian's Nix binary cache.
+  hadrian.cache.enable = true;
+
+  nix.settings.trusted-users = [ "@wheel" ];
+
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  # Additionall exit node settings that Tailscale recommends.
+  networking.firewall.checkReversePath = "loose";
+
+  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 = "kyoko";
+    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;
+
+    printing = {
+      enable = true;
+      drivers = with pkgs; [ gutenprint ];
+    };
+
+    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 = with wpcarro.keys; [
+    iphone
+    nathan
+    tarasco
+  ];
+  users.users.wpcarro = {
+    initialPassword = "password";
+    isNormalUser = true;
+    extraGroups = [
+      "networkmanager"
+      "wheel"
+      "docker"
+    ];
+    shell = pkgs.fish;
+    openssh.authorizedKeys.keys = with wpcarro.keys; [
+      iphone
+      nathan
+      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
+      ec2-api-tools
+      firefox
+      google-chrome
+      httpie
+      pavucontrol
+      quasselClient
+      remmina
+      tdesktop
+      wpcarrosEmacs
+      xsecurelock
+    ]);
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/wpcarro/nixos/kyoko/kyoko.el b/users/wpcarro/nixos/kyoko/kyoko.el
new file mode 100644
index 000000000000..310323688a7e
--- /dev/null
+++ b/users/wpcarro/nixos/kyoko/kyoko.el
@@ -0,0 +1,61 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'bookmark)
+(require 'display)
+(require 'window-manager)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(bookmark-install-kbd
+ (make-bookmark :label "hadrian"
+                :path "/hadrian"
+                :kbd "h"))
+
+(setq initial-buffer-choice "/hadrian")
+
+(add-to-list 'ssh-hosts "wpcarro@tarasco")
+
+(display-register primary
+                  :output "DP-2"
+                  :primary t
+                  :coords (0 0)
+                  :size (2560 1440)
+                  :rate 30.0
+                  :dpi 96
+                  :rotate normal)
+
+(display-register secondary
+                  :output "DP-1"
+                  :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)))
+
+;; I *think* this needs to be the last statement in this file.
+(window-manager-init :init-hook #'display-arrange-main)
diff --git a/users/wpcarro/nixos/marcus/default.nix b/users/wpcarro/nixos/marcus/default.nix
new file mode 100644
index 000000000000..355b50e17422
--- /dev/null
+++ b/users/wpcarro/nixos/marcus/default.nix
@@ -0,0 +1,169 @@
+{ depot, pkgs, lib, ... }:
+{ ... }:
+
+let
+  inherit (depot.users) wpcarro;
+  inherit (depot.users.wpcarro.lib) usermod;
+
+  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")
+    (usermod "laptop.nix")
+  ];
+
+  # 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;
+  };
+
+  services = wpcarro.common.services // {
+    # Enable the Tailscale daemon to connect to work and personal Tailnet.
+    tailscale.enable = true;
+
+    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;
+
+  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;
+
+    # Previous default version, see https://github.com/nix-community/home-manager/blob/master/docs/release-notes/rl-2211.adoc
+    home.stateVersion = "18.09";
+  };
+
+  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 000000000000..8a2672206b50
--- /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/nixos";
+    fsType = "ext4";
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-label/boot";
+    fsType = "vfat";
+  };
+
+  swapDevices = lib.singleton {
+    device = "/dev/disk/by-label/swap";
+  };
+
+  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 000000000000..90c04f7ff31a
--- /dev/null
+++ b/users/wpcarro/nixos/marcus/marcus.el
@@ -0,0 +1,40 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'tvl)
+(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 initial-buffer-choice tvl-depot-path)
+
+(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/modules/.skip-subtree b/users/wpcarro/nixos/modules/.skip-subtree
new file mode 100644
index 000000000000..09520f8c831f
--- /dev/null
+++ b/users/wpcarro/nixos/modules/.skip-subtree
@@ -0,0 +1 @@
+NixOS modules are not readTree compatible.
diff --git a/users/wpcarro/nixos/modules/hadrian-cache.nix b/users/wpcarro/nixos/modules/hadrian-cache.nix
new file mode 100644
index 000000000000..033c03c825b7
--- /dev/null
+++ b/users/wpcarro/nixos/modules/hadrian-cache.nix
@@ -0,0 +1,17 @@
+# If enabled, use Hadrian's Nix cache.
+{ config, lib, pkgs, ... }:
+
+{
+  options = {
+    hadrian.cache.enable = lib.mkEnableOption "Hadrian's binary cache";
+  };
+
+  config = lib.mkIf config.hadrian.cache.enable {
+    nix.settings.trusted-public-keys = [
+      "cache.hadrian.internal:XWdYSn5ZASj6IqZd4nnDBXJmahQEolBrtq9DvSe0UT0="
+    ];
+    nix.settings.substituters = [
+      "http://cache.hadrian.internal"
+    ];
+  };
+}
diff --git a/users/wpcarro/nixos/modules/hardware/dell-emc-egw-5200.nix b/users/wpcarro/nixos/modules/hardware/dell-emc-egw-5200.nix
new file mode 100644
index 000000000000..df46405629c7
--- /dev/null
+++ b/users/wpcarro/nixos/modules/hardware/dell-emc-egw-5200.nix
@@ -0,0 +1,47 @@
+# In a nutshell, this configuration defines the configuration required to run
+# NixOS on the Dell EMC EGW 5200 (often the config that NixOS put in
+# hardware.nix by default).
+{ config, lib, modulesPath, ... }:
+
+{
+  imports = [
+    (modulesPath + "/installer/scan/not-detected.nix")
+  ];
+
+  boot.initrd.availableKernelModules = [
+    "xhci_pci"
+    "ahci"
+    "usb_storage"
+    "usbhid"
+    "sd_mod"
+  ];
+  boot.initrd.kernelModules = [ ];
+  boot.kernelModules = [ "kvm-intel" ];
+  boot.extraModulePackages = [ ];
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  fileSystems."/" = {
+    device = "/dev/disk/by-label/NIXROOT";
+    fsType = "ext4";
+  };
+
+  fileSystems."/boot" = {
+    device = "/dev/disk/by-label/NIXBOOT";
+    fsType = "vfat";
+  };
+
+  swapDevices = [ ];
+
+  powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
+  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+
+  # Needed for Tailscale subnet routing
+  boot.kernel.sysctl."net.ipv4.ip_forward" = 1;
+  networking.useDHCP = false;
+  networking.interfaces.eno1.useDHCP = true;
+  networking.interfaces.enp3s0.useDHCP = true;
+  networking.interfaces.enp4s0.useDHCP = true;
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/wpcarro/nixos/modules/hardware/nopn.nix b/users/wpcarro/nixos/modules/hardware/nopn.nix
new file mode 100644
index 000000000000..a3569542126f
--- /dev/null
+++ b/users/wpcarro/nixos/modules/hardware/nopn.nix
@@ -0,0 +1,53 @@
+# I tried looking up the manufacturer, product name, and version, but
+# `dmidecode -t system` reported "To be filled by O.E.M." for each of these
+# fields.
+{ 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";
+  };
+
+  boot = {
+    initrd.availableKernelModules = [
+      "xhci_pci"
+      "ehci_pci"
+      "ahci"
+      "usb_storage"
+      "usbhid"
+      "sd_mod"
+    ];
+    initrd.kernelModules = [ ];
+    kernelModules = [ "kvm-intel" ];
+    extraModulePackages = [ ];
+
+    # 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"
+    ];
+  };
+
+  swapDevices = [ ];
+
+  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+  # TODO(wpcarro): https://github.com/NixOS/nixpkgs/issues/222805
+  # high-resolution display
+  # hardware.video.hidpi.enable = lib.mkDefault true;
+}
diff --git a/users/wpcarro/nixos/modules/laptop.nix b/users/wpcarro/nixos/modules/laptop.nix
new file mode 100644
index 000000000000..03dd0f39bb81
--- /dev/null
+++ b/users/wpcarro/nixos/modules/laptop.nix
@@ -0,0 +1,15 @@
+# Laptop-specific NixOS configuration.
+_:
+
+{
+  # Automatically detect location for redshift.
+  services.geoclue2.enable = true;
+  location.provider = "geoclue2";
+
+  # Enable power-saving features.
+  powerManagement.powertop.enable = true;
+
+  # Backlight control command.
+  programs.light.enable = true;
+}
+
diff --git a/users/wpcarro/nixos/modules/nginx.nix b/users/wpcarro/nixos/modules/nginx.nix
new file mode 100644
index 000000000000..e6cc6b0febab
--- /dev/null
+++ b/users/wpcarro/nixos/modules/nginx.nix
@@ -0,0 +1,45 @@
+# Common configuration for Nginx.
+{ pkgs, ... }:
+
+{
+  config = {
+    security.acme = {
+      acceptTerms = true;
+      defaults.email = "wpcarro@gmail.com";
+    };
+
+    services.nginx = {
+      enable = true;
+      enableReload = true;
+
+      recommendedTlsSettings = true;
+      recommendedGzipSettings = true;
+
+      # Log errors to journald (i.e. /dev/log) with debug verbosity.
+      logError = "syslog:server=unix:/dev/log debug";
+
+      # 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;
+      '';
+
+      appendHttpConfig = ''
+        add_header Permissions-Policy "interest-cohort=()";
+      '';
+    };
+  };
+}
diff --git a/users/wpcarro/nixos/tarasco/default.nix b/users/wpcarro/nixos/tarasco/default.nix
new file mode 100644
index 000000000000..a888ab6b4ead
--- /dev/null
+++ b/users/wpcarro/nixos/tarasco/default.nix
@@ -0,0 +1,144 @@
+{ depot, pkgs, lib, ... }:
+{ ... }:
+
+let
+  inherit (depot.users) wpcarro;
+  inherit (depot.users.wpcarro.lib) usermod;
+
+  wpcarrosEmacs = wpcarro.emacs.nixos {
+    load = [ ./tarasco.el ];
+  };
+
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+in
+{
+  imports = [
+    (usermod "hardware/nopn.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;
+  };
+
+
+  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 = with wpcarro.keys; [
+    ava
+    iphone
+    nathan
+  ];
+  users.users.wpcarro = {
+    isNormalUser = true;
+    extraGroups = [
+      "networkmanager"
+      "wheel"
+      "docker"
+    ];
+    shell = pkgs.fish;
+    openssh.authorizedKeys.keys = with wpcarro.keys; [
+      ava
+      iphone
+      nathan
+    ];
+  };
+  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/tarasco.el b/users/wpcarro/nixos/tarasco/tarasco.el
new file mode 100644
index 000000000000..c840493f243a
--- /dev/null
+++ b/users/wpcarro/nixos/tarasco/tarasco.el
@@ -0,0 +1,61 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Dependencies
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(require 'bookmark)
+(require 'display)
+(require 'window-manager)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Configuration
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(bookmark-install-kbd
+ (make-bookmark :label "hadrian"
+                :path "/hadrian"
+                :kbd "h"))
+
+(setq initial-buffer-choice "/hadrian")
+
+(add-to-list 'ssh-hosts "wpcarro@ava")
+
+(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)))
+
+;; I *think* this needs to be the last statement in this file.
+(window-manager-init :init-hook #'display-arrange-main)
diff --git a/users/wpcarro/playbooks/README.md b/users/wpcarro/playbooks/README.md
new file mode 100644
index 000000000000..70a26c8e8914
--- /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 000000000000..7bce39ca76bc
--- /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 000000000000..aac63735d9c4
--- /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 000000000000..d292ef832c23
--- /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 000000000000..80749a3c6b81
--- /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 000000000000..9d111cf6bba5
--- /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 000000000000..1f473b5f59fa
--- /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 000000000000..54f8e5f34462
--- /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 000000000000..5eda417f489c
--- /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 000000000000..aec87f0b59ee
--- /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 000000000000..8259ac70d9b2
--- /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 000000000000..e7c105a7f60f
--- /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 000000000000..bd4024e3ec7d
--- /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 000000000000..77774c1bb5ad
--- /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 000000000000..6dd863528c1c
--- /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 000000000000..adef73b452dc
--- /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 000000000000..3d82846e6126
--- /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 000000000000..aba99b8239ff
--- /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 000000000000..14597d5104e3
--- /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 000000000000..c02f9a9c8108
--- /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 000000000000..e7b627613389
--- /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 000000000000..e5310565dbbd
--- /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/compiler/.envrc b/users/wpcarro/scratch/compiler/.envrc
new file mode 100644
index 000000000000..ff7eea1f7a05
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/.envrc
@@ -0,0 +1,3 @@
+source_up
+
+use_nix
diff --git a/users/wpcarro/scratch/compiler/.gitignore b/users/wpcarro/scratch/compiler/.gitignore
new file mode 100644
index 000000000000..96261d3fc7e9
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/.gitignore
@@ -0,0 +1,5 @@
+a.out
+*.cmi
+*.cmo
+*.cmx
+*.o
\ No newline at end of file
diff --git a/users/wpcarro/scratch/compiler/debug.ml b/users/wpcarro/scratch/compiler/debug.ml
new file mode 100644
index 000000000000..e39ff13742be
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/debug.ml
@@ -0,0 +1,66 @@
+open Types
+
+(* Print x prefixed with tag and return x unchanged. *)
+let print (f : 'a -> string) (tag : string) (x : 'a) : 'a =
+  Printf.printf "%s: %s\n" tag (f x);
+  x
+
+let rec ast (tree : Types.value) : string =
+  match tree with
+  | ValueLiteral (LiteralBool x) ->
+     Printf.sprintf "ValueLiteral (LiteralBool %s)" (string_of_bool x)
+  | ValueLiteral (LiteralInt x) ->
+     Printf.sprintf "ValueLiteral (LiteralInt %s)" (string_of_int x)
+  | ValueVariable x ->
+     Printf.sprintf "ValueVariable %s" x
+  | ValueFunction (x, body) ->
+     Printf.sprintf "ValueFunction (%s, %s)" x (ast body)
+  | ValueApplication (f, x) ->
+     Printf.sprintf "ValueApplication (%s, %s)" (ast f) (ast x)
+  | ValueVarApplication (f, x) ->
+     Printf.sprintf "ValueVarApplication (%s, %s)" f (ast x)
+  | ValueBinder (k, v, x) ->
+      Printf.sprintf "ValueBinder (%s, %s, %s)" k (ast v) (ast x)
+
+let rec value (x : value) : string =
+  match x with
+  | ValueLiteral (LiteralInt x) ->
+     Printf.sprintf "Int %d" x
+  | ValueLiteral (LiteralBool x) ->
+     Printf.sprintf "Bool %b" x
+  | ValueVariable x ->
+     Printf.sprintf "Var %s" x
+  | ValueFunction (name, x) ->
+     Printf.sprintf "Fn %s %s" name (value x)
+  | ValueApplication (f, x) ->
+     Printf.sprintf "App %s %s" (value f) (value x)
+  | ValueVarApplication (name, x) ->
+     Printf.sprintf "App %s %s" name (value x)
+  | ValueBinder (name, x, body) ->
+     Printf.sprintf "Bind %s %s %s" name (value x) (value body)
+
+let rec type' (t : _type) : string =
+  match t with
+  | TypeInt -> "Integer"
+  | TypeBool -> "Boolean"
+  | TypeVariable k -> Printf.sprintf "%s" k
+  | TypeArrow (a, b) -> Printf.sprintf "%s -> %s" (type' a) (type' b)
+
+let quantified_type (q : quantified_type) : string =
+  let QuantifiedType (vars, t) = q in
+  if List.length vars == 0 then
+    Printf.sprintf "%s" (type' t)
+  else
+    Printf.sprintf "forall %s. %s" (String.concat "," vars) (type' t)
+
+let substitution (s : substitution) : string =
+  FromString.fold (fun k v acc -> Printf.sprintf "%s\"%s\" |-> %s;" acc k (type' v)) s ""
+  |> Printf.sprintf "{ %s }"
+
+let env (s : env) : string =
+  FromString.fold (fun k v acc -> Printf.sprintf "%s\"%s\" |-> %s;" acc k (quantified_type v)) s ""
+  |> Printf.sprintf "{ %s }"
+
+let inference (Inference (s, t)) =
+  Printf.sprintf "type: %s; sub: %s" (type' t) (substitution s)
+
diff --git a/users/wpcarro/scratch/compiler/expr_parser.ml b/users/wpcarro/scratch/compiler/expr_parser.ml
new file mode 100644
index 000000000000..797592931a2c
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/expr_parser.ml
@@ -0,0 +1,187 @@
+(*******************************************************************************
+ * CLI REPL for an s-expression Lambda Calculus.
+ *
+ * Lambda Calculus Expression Language:
+ *
+ *   Helpers:
+ *     symbol     -> [-a-z]+
+ *     string     -> '"' [^"]* '"'
+ *     boolean    -> 'true' | 'false'
+ *     integer    -> [1-9][0-9]*
+ *
+ *   Core:
+ *     expression -> funcdef
+ *     binding    -> '(' 'let' symbol expr expr ')'
+ *     funcdef    -> '(' 'fn' symbol expr ')'
+ *     funccall   -> '(' ( symbol | funcdef) expr ')'
+ *     literal    -> string | boolean | integer
+ *     variable   -> symbol
+ *
+ * Example Usage:
+ *   $ ocamlopt types.ml str.cmxa inference.ml parser.ml expr_parser.ml && ./a.out
+ *   repl> true
+ *   tokens: [ "true" ]
+ *   ast: ValueLiteral (LiteralBool true)
+ *   Boolean
+ *   repl>
+ *
+ ******************************************************************************)
+
+open Parser
+open Inference
+open Debug
+open Prettify
+open Vec
+
+type literal = LiteralBool of bool | LiteralInt of int
+
+let ( let* ) = Option.bind
+let map = Option.map
+
+let tokenize (x : string) : token vec =
+  let xs = Vec.create () in
+  let i = ref 0 in
+  while !i < String.length x do
+    match x.[!i] with
+    | ' ' -> i := !i + 1
+    (* strings *)
+    | '"' ->
+      let curr = ref "\"" in 
+      i := !i + 1;
+      while x.[!i] != '"' do
+        curr := !curr ^ "?";
+        i := !i + 1
+      done;
+      curr := !curr ^ "\"";
+      Vec.append !curr xs;
+      i := !i + 1
+    | '(' ->
+        Vec.append "(" xs;
+        i := !i + 1
+    | ')' ->
+        Vec.append ")" xs;
+        i := !i + 1
+    | _ ->
+        let token = ref "" in
+        while !i < String.length x && not (String.contains "() " x.[!i]) do
+          token := !token ^ String.make 1 x.[!i];
+          i := !i + 1
+        done;
+        Vec.append !token xs
+  done;
+  xs
+
+let parse_symbol (p : parser) : string option =
+  let* x = p#curr in
+  if Str.string_match (Str.regexp "[-a-z][0-9]*") x 0 then
+    begin
+      p#advance;
+      Some x
+    end
+  else
+    None
+
+let parse_variable (p : parser) : Types.value option =
+  let* x = parse_symbol p in
+  Some (Types.ValueVariable x)
+
+let parse_literal (p : parser) : Types.value option =
+  match p#curr with
+  | Some "true" ->
+     p#advance;
+     Some (ValueLiteral (LiteralBool true))
+  | Some "false" ->
+     p#advance;
+     Some (ValueLiteral (LiteralBool false))
+  | Some x ->
+     (match int_of_string_opt x with
+      | Some n ->
+         p#advance;
+         Some (ValueLiteral (LiteralInt n))
+      | _ -> 
+        if String.starts_with ~prefix:"\"" x then
+          begin
+            p#advance;
+            Some (ValueLiteral (LiteralString x))
+          end
+        else
+          parse_variable p)
+  | _ -> None
+
+let rec parse_expression (p : parser) : Types.value option =
+  parse_binding p
+
+and parse_funccall (p : parser) : Types.value option =
+  match (p#curr, p#next) with
+  | (Some "(", Some "(") ->
+     p#advance;
+     let* f = parse_funcdef p in
+     let* x = parse_expression p in
+     p#expect ")";
+     Some (Types.ValueApplication (f, x))
+  | (Some "(", _) ->
+     p#advance;
+     let* f = parse_symbol p in
+     let* x = parse_expression p in
+     p#expect ")";
+     Some (Types.ValueVarApplication (f, x))
+  | _ -> parse_literal p
+
+and parse_funcdef (p : parser) : Types.value option =
+  match (p#curr, p#next) with
+  | (Some "(", Some "fn") ->
+     p#advance;
+     p#advance;
+     let* name = parse_symbol p in
+     let* body = parse_expression p in
+     p#expect ")";
+     Some (Types.ValueFunction (name, body))
+  | _ -> parse_funccall p
+
+and parse_binding (p : parser) : Types.value option =
+  match (p#curr, p#next) with
+  | (Some "(", Some "let") ->
+     p#advance;
+     p#advance;
+     let* name = parse_symbol p in
+     let* value = parse_expression p in
+     let* body = parse_expression p in
+     Some (Types.ValueBinder (name, value, body))
+  | _ -> parse_funcdef p
+
+let print_tokens (xs : string vec) : unit =
+  xs 
+  |> Vec.map (Printf.sprintf "\"%s\"")
+  |> Vec.join ", "
+  |> Printf.sprintf "tokens: [ %s ]"
+  |> print_string 
+  |> print_newline
+
+let parse_language (x : string) : Types.value option =
+  let tokens = tokenize x in
+  print_tokens tokens;
+  parse_expression (new parser tokens)
+
+let main =
+  while true do
+    begin
+      print_string "repl> ";
+      let x = read_line () in
+      match parse_language x with
+      | Some ast ->
+         (match ast |> Debug.print Debug.ast "ast" |> do_infer with
+          | None ->
+             "Type-check failed"
+             |> print_string
+             |> print_newline
+          | Some x ->
+             x
+             |> Prettify.type'
+             |> print_string
+             |> print_newline)
+      | None ->
+         "Could not parse"
+         |> print_string
+         |> print_newline
+    end
+  done
diff --git a/users/wpcarro/scratch/compiler/inference.ml b/users/wpcarro/scratch/compiler/inference.ml
new file mode 100644
index 000000000000..e00904a09eed
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/inference.ml
@@ -0,0 +1,183 @@
+(*******************************************************************************
+ * WIP implementation of the Hindley-Milner type system primarily for learning
+ * purposes.
+ *
+ * Wish List:
+ * - TODO Debug this inference (let f (fn x x) f)
+ ******************************************************************************)
+
+open Types
+open Debug
+
+(*******************************************************************************
+ * Library
+ ******************************************************************************)
+
+let ( let* ) = Option.bind
+
+let set_from_list (xs : string list) : set =
+  xs |> List.fold_left (fun acc x -> FromString.add x true acc) FromString.empty
+
+(* Map union that favors the rhs values (i.e. "last writer wins"). *)
+let lww (xs : 'a FromString.t) (ys : 'a FromString.t) : 'a FromString.t =
+  FromString.union (fun k x y -> Some y) xs ys
+
+let emptyEnv : env = FromString.empty
+
+let rec free_type_vars (t : _type) : set =
+  match t with
+  | TypeVariable k -> FromString.singleton k true
+  | TypeInt -> FromString.empty
+  | TypeBool -> FromString.empty
+  | TypeString -> FromString.empty
+  | TypeArrow (a, b) -> lww (free_type_vars a) (free_type_vars b)
+
+let i : int ref = ref 0
+
+let make_type_var () : _type =
+  let res = Printf.sprintf "a%d" !i in
+  i := !i + 1;
+  TypeVariable res
+
+exception OccursCheck
+
+let bind_var (k : string) (t : _type) : substitution =
+  if t == TypeVariable k then FromString.empty
+  else if FromString.exists (fun name _ -> name == k) (free_type_vars t) then
+    raise OccursCheck
+  else FromString.singleton k t
+
+let rec instantiate (q : quantified_type) : _type =
+  let (QuantifiedType (names, t)) = q in
+  match t with
+  | TypeInt -> TypeInt
+  | TypeBool -> TypeBool
+  | TypeString -> TypeString
+  | TypeVariable k ->
+      if List.exists (( == ) k) names then make_type_var () else TypeVariable k
+  | TypeArrow (a, b) ->
+      TypeArrow
+        (instantiate (QuantifiedType (names, a)), instantiate (QuantifiedType (names, b)))
+
+let quantified_type_ftvs (q : quantified_type) : set =
+  let (QuantifiedType (names, t)) = q in
+  lww (free_type_vars t) (names |> set_from_list)
+
+let generalize (env : env) (t : _type) : quantified_type =
+  let envftv =
+    env |> FromString.bindings
+    |> List.map (fun (_, v) -> quantified_type_ftvs v)
+    |> List.fold_left lww FromString.empty
+  in
+  let names =
+    lww (free_type_vars t) envftv
+    |> FromString.bindings
+    |> List.map (fun (k, _) -> k)
+  in
+  QuantifiedType (names, t)
+
+let rec substitute_type (s : substitution) (t : _type) : _type =
+  match t with
+  | TypeVariable k as tvar ->
+     (match FromString.find_opt k s with
+      | Some v -> substitute_type s v
+      | None -> tvar)
+  | TypeArrow (a, b) -> TypeArrow (substitute_type s a, substitute_type s b)
+  | TypeInt -> TypeInt
+  | TypeBool -> TypeBool
+  | TypeString -> TypeString
+
+let substitute_quantified_type (s : substitution) (q : quantified_type) : quantified_type =
+  let (QuantifiedType (names, t)) = q in
+  let s1 =
+    FromString.filter (fun k v -> List.exists (fun x -> k != x) names) s
+  in
+  QuantifiedType (names, substitute_type s1 t)
+
+let substitute_env (s : substitution) (env : env) : env =
+  FromString.map (fun q -> substitute_quantified_type s q) env
+
+let compose_substitutions (xs : substitution list) : substitution =
+  let do_compose_substitutions s1 s2 = lww s2 (FromString.map (substitute_type s2) s1) in
+  List.fold_left do_compose_substitutions FromString.empty xs
+
+let rec unify (a : _type) (b : _type) : substitution option =
+  match (a, b) with
+  | TypeInt, TypeInt -> Some FromString.empty
+  | TypeBool, TypeBool -> Some FromString.empty
+  | TypeString, TypeString -> Some FromString.empty
+  | TypeVariable k, _ -> Some (bind_var k b)
+  | _, TypeVariable k -> Some (bind_var k a)
+  | TypeArrow (a, b), TypeArrow (c, d) ->
+      let* s1 = unify a c in
+      let* s2 = unify (substitute_type s1 b) (substitute_type s1 d) in
+      let s3 = compose_substitutions [s1; s2] in
+      s1 |> Debug.substitution |> Printf.sprintf "s1: %s\n" |> print_string;
+      s2 |> Debug.substitution |> Printf.sprintf "s2: %s\n" |> print_string;
+      s3 |> Debug.substitution |> Printf.sprintf "s3: %s\n" |> print_string;
+      Some s3
+  | _ -> None
+
+let print_env (env : env) =
+  Printf.sprintf "env: %s\n" (Debug.env env)
+  |> print_string
+
+let print_val (x : value) =
+  Printf.sprintf "val: %s\n" (Debug.value x)
+  |> print_string
+
+let print_inference (x : inference option) =
+  match x with
+  | None -> "no inference\n" |> print_string
+  | Some x ->
+     Printf.sprintf "inf: %s\n" (Debug.inference x)
+     |> print_string
+
+let rec infer (env : env) (x : value) : inference option =
+  print_env env;
+  print_val x;
+  let res = match x with
+  | ValueLiteral lit -> (
+      match lit with
+      | LiteralInt _ -> Some (Inference (FromString.empty, TypeInt))
+      | LiteralBool _ -> Some (Inference (FromString.empty, TypeBool))
+      | LiteralString _ -> Some (Inference (FromString.empty, TypeString)))
+  | ValueVariable k ->
+      let* v = FromString.find_opt k env in
+      Some (Inference (FromString.empty, instantiate v))
+  | ValueFunction (param, body) ->
+      let typevar = make_type_var () in
+      let env1 = FromString.remove param env in
+      let env2 = lww (FromString.singleton param (QuantifiedType ([], typevar))) env1 in
+      let* (Inference (s1, t1)) = infer env2 body in
+      Some (Inference (s1, TypeArrow (substitute_type s1 typevar, t1)))
+  | ValueApplication (f, x) ->
+      let result = make_type_var () in
+      let* (Inference (s1, t1)) = infer env f in
+      let* (Inference (s2, t2)) = infer (substitute_env s1 env) x in
+      let* s3 = unify (substitute_type s2 t1) (TypeArrow (t2, result)) in
+      Some (Inference
+              ( compose_substitutions [s3; s2; s1],
+                substitute_type s3 result ))
+  | ValueVarApplication (name, x) ->
+      let* v = FromString.find_opt name env in
+      let t1 = instantiate v in
+      let typevar = make_type_var () in
+      let* (Inference (s2, t2)) = infer env x in
+      let* s3 = unify (substitute_type s2 t1) (TypeArrow (t2, typevar)) in
+      Some (Inference
+              ( compose_substitutions [s2; s3],
+                substitute_type s3 typevar ))
+  | ValueBinder (k, v, body) ->
+      let* (Inference (s1, t1)) = infer env v in
+      let env1 = FromString.remove k env in
+      let tg = generalize (substitute_env s1 env) t1 in
+      let env2 = FromString.add k tg env1 in
+      let* (Inference (s2, t2)) = infer (substitute_env s1 env2) body in
+      Some (Inference (compose_substitutions [s1; s2], t2)) in
+  print_inference res;
+  res
+
+let do_infer (x : value) : _type option =
+  let* Inference (_, t) = infer FromString.empty x in
+  Some t
diff --git a/users/wpcarro/scratch/compiler/parser.ml b/users/wpcarro/scratch/compiler/parser.ml
new file mode 100644
index 000000000000..dc66f2506ed3
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/parser.ml
@@ -0,0 +1,47 @@
+(****************************************************************************** 
+ * Defines a generic parser class.
+ ******************************************************************************)
+
+open Vec
+
+exception ParseError of string
+
+type token = string
+type state = { i : int; tokens : token vec }
+
+class parser (tokens : token vec) =
+  object (self)
+    val mutable tokens = tokens
+    val mutable i = ref 0
+
+    method advance = i := !i + 1
+    method prev : token option = Vec.get (!i - 1) tokens
+    method curr : token option = Vec.get !i tokens
+    method next : token option = Vec.get (!i + 1) tokens
+
+    method consume : token option =
+      match self#curr with
+      | None -> None
+      | Some x as res ->
+          self#advance;
+          res
+
+    method expect (x : token) =
+      match self#curr with
+      | Some y when x = y -> self#advance
+      | _ -> raise (ParseError (Printf.sprintf "Expected %s" x))
+
+    method matches (x : token) : bool =
+      match self#curr with
+      | None -> false
+      | Some y ->
+          if x = y then
+            begin
+              self#advance;
+              true
+            end
+          else false
+
+    method exhausted : bool = !i >= Vec.length tokens
+    method state : state = { i = !i; tokens }
+  end
diff --git a/users/wpcarro/scratch/compiler/prettify.ml b/users/wpcarro/scratch/compiler/prettify.ml
new file mode 100644
index 000000000000..7903ad36947c
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/prettify.ml
@@ -0,0 +1,9 @@
+open Types
+
+(* Pretty-print the type, t. *)
+let rec type' (t : _type) : string =
+  match t with
+  | TypeInt -> "Integer"
+  | TypeBool -> "Boolean"
+  | TypeVariable k -> Printf.sprintf "%s" k
+  | TypeArrow (a, b) -> Printf.sprintf "%s -> %s" (type' a) (type' b)
diff --git a/users/wpcarro/scratch/compiler/register_vm.ml b/users/wpcarro/scratch/compiler/register_vm.ml
new file mode 100644
index 000000000000..0a573048e77e
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/register_vm.ml
@@ -0,0 +1,129 @@
+(*
+  Rewriting the Python implementation of the register VM in OCaml to see how
+  how much imperative/mutative programming OCaml allows.
+
+  Note: Some of this code is intentionally not written in a functional style
+  because one of the goals was to see how similar this OCaml implementation
+  could be to the Python implementation.
+
+  Conclusion: It's pretty easy to switch between the two languages.
+
+  Usage: Recommended compilation settings I hastily found online:
+  $ ocamlopt -w +A-42-48 -warn-error +A-3-44 ./register_vm.ml && ./a.out
+
+  Formatting:
+  $ ocamlformat --inplace --enable-outside-detected-project ./register_vm.ml
+ *)
+
+open Vec
+
+type reg = X | Y | Res
+type binop = int -> int -> int
+
+type ast =
+  | Const of int
+  | Add of ast * ast
+  | Sub of ast * ast
+  | Mul of ast * ast
+  | Div of ast * ast
+
+type opcode0 =
+  | Op0AssignRegLit of reg * int
+  | Op0AssignRegReg of reg * reg
+  | Op0BinOp of binop * reg * reg * reg
+  | Op0PushReg of reg
+  | Op0PopAndSet of reg
+  | Op0Null
+
+type opcode1 =
+  | Op1AssignRegLit of int * int
+  | Op1AssignRegReg of int * int
+  | Op1BinOp of (int -> int -> int) * int * int * int
+  | Op1PushReg of int
+  | Op1PopAndSet of int
+  | Op1Null
+
+type opcodes0 = opcode0 vec
+type opcodes1 = opcode1 vec
+
+let registers : int vec = Vec.make 8 0
+let stack : int Stack.t = Stack.create ()
+let reg_idx (r : reg) : int = match r with X -> 0 | Y -> 1 | Res -> 2
+
+let reg_name (r : reg) : string =
+  match r with X -> "x" | Y -> "y" | Res -> "res"
+
+let print_opcodes0 (xs : opcodes0) : opcodes0 =
+  let print_opcode x =
+    match x with
+    | Op0AssignRegLit (r, x) -> Printf.printf "%s <- %d\n" (reg_name r) x
+    | Op0AssignRegReg (dst, src) ->
+        Printf.printf "%s <- $%s\n" (reg_name dst) (reg_name src)
+    | Op0PushReg src -> Printf.printf "push $%s\n" (reg_name src)
+    | Op0PopAndSet dst -> Printf.printf "%s <- pop\n" (reg_name dst)
+    | Op0BinOp (_, lhs, rhs, dst) ->
+        Printf.printf "%s <- $%s ? $%s\n" (reg_name dst) (reg_name lhs)
+          (reg_name rhs)
+    | Op0Null -> ()
+  in
+  Vec.iter print_opcode xs;
+  xs
+
+let rec compile (ast : ast) : opcodes0 =
+  let result : opcodes0 = Vec.create () in
+  (match ast with
+   | Const x -> Vec.append (Op0AssignRegLit (Res, x)) result;
+   | Add (lhs, rhs) -> compile_bin_op ( + ) lhs rhs result
+   | Sub (lhs, rhs) -> compile_bin_op ( - ) lhs rhs result
+   | Mul (lhs, rhs) -> compile_bin_op ( * ) lhs rhs result
+   | Div (lhs, rhs) -> compile_bin_op ( / ) lhs rhs result);
+  result
+
+and compile_bin_op (f : binop) (lhs : ast) (rhs : ast) (result : opcodes0) =
+  lhs |> compile |> Vec.append_to result;
+  Vec.append (Op0PushReg Res) result;
+  rhs |> compile |> Vec.append_to result;
+  Vec.append (Op0PopAndSet X) result;
+  Vec.append (Op0AssignRegReg (Y, Res)) result;
+  Vec.append (Op0BinOp (f, X, Y, Res)) result
+
+let compile_registers (xs : opcodes0) : opcodes1 =
+  let do_compile x =
+    match x with
+    | Op0AssignRegLit (dst, x) -> Op1AssignRegLit (reg_idx dst, x)
+    | Op0AssignRegReg (dst, src) -> Op1AssignRegReg (reg_idx dst, reg_idx src)
+    | Op0PushReg src -> Op1PushReg (reg_idx src)
+    | Op0PopAndSet dst -> Op1PopAndSet (reg_idx dst)
+    | Op0BinOp (f, lhs, rhs, dst) -> Op1BinOp (f, reg_idx lhs, reg_idx rhs, reg_idx dst)
+    | Op0Null -> Op1Null
+  in
+  Vec.map do_compile xs
+
+let eval (xs : opcodes1) : int =
+  let ip = ref 0 in
+  while !ip < Vec.length xs do
+    match Vec.get_unsafe !ip xs with
+    | Op1AssignRegLit (dst, x) ->
+        Vec.set dst x registers;
+        ip := !ip + 1
+    | Op1AssignRegReg (dst, src) ->
+        Vec.set dst (Vec.get_unsafe src registers) registers;
+        ip := !ip + 1
+    | Op1PushReg src ->
+        Stack.push (Vec.get_unsafe src registers) stack;
+        ip := !ip + 1
+    | Op1PopAndSet dst ->
+        Vec.set dst (Stack.pop stack) registers;
+        ip := !ip + 1
+    | Op1BinOp (f, lhs, rhs, dst) ->
+        let lhs = Vec.get_unsafe lhs registers in
+        let rhs = Vec.get_unsafe rhs registers in
+        Vec.set dst (f lhs rhs) registers;
+        ip := !ip + 1
+    | Op1Null -> ip := !ip + 1
+  done;
+  Vec.get_unsafe (reg_idx Res) registers
+;;
+
+Add (Mul (Const 2, Div (Const 100, Const 2)), Const 5)
+|> compile |> print_opcodes0 |> compile_registers |> eval |> print_int
diff --git a/users/wpcarro/scratch/compiler/register_vm.py b/users/wpcarro/scratch/compiler/register_vm.py
new file mode 100644
index 000000000000..302bce5a0ecb
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/register_vm.py
@@ -0,0 +1,161 @@
+# Silly proof-of-concept register VM.
+
+def compile_binary_op(op, ast):
+    result = []
+    for x in compile(ast[1]):
+        result.append(x)
+    result.append(PUSH_REG)
+    result.append(RES)
+    for x in compile(ast[2]):
+        result.append(x)
+    result.append(ASSIGN_REG_REG)
+    result.append(Y)
+    result.append(RES)
+    result.append(POP)
+    result.append(X)
+    result.append(op)
+    return result
+
+def compile(ast):
+    result = []
+
+    if ast[0] == 'CONST':
+        result.append(ASSIGN_REG_LIT)
+        result.append(RES)
+        result.append(ast[1])
+    elif ast[0] == 'ADD':
+        result += compile_binary_op(ADD, ast)
+    elif ast[0] == 'SUB':
+        result += compile_binary_op(SUB, ast)
+    elif ast[0] == 'MUL':
+        result += compile_binary_op(MUL, ast)
+    elif ast[0] == 'DIV':
+        result += compile_binary_op(DIV, ast)
+    elif ast[0] == 'RETURN':
+        result.append(RETURN)
+    else:
+        raise Exception('Cannot compile unknown AST node: {}'.format(ast[0]))
+
+    return result
+
+# opcodes
+ASSIGN_REG_LIT = 0x0
+ASSIGN_REG_REG = 0x1
+ADD = 0x2
+SUB = 0x3
+MUL = 0x4
+DIV = 0x5
+SWAP = 0x6
+RETURN = 0x7
+PUSH_REG = 0x8
+POP = 0x9
+
+# register indices
+X = 0x0
+Y = 0x1
+RES = 0x2
+
+registers = [0x0] * 8
+stack = []
+
+def reg_name(i):
+    if i == X: return 'x'
+    if i == Y: return 'x'
+    if i == RES: return 'res'
+
+def print_instructions(xs):
+    i = 0
+
+    while i < len(xs):
+        if xs[i] == ASSIGN_REG_LIT:
+            # print('ASSIGN_REG_LIT {} {}'.format(reg_name(xs[i + 1]), xs[i + 2]))
+            print('{} <- {}'.format(reg_name(xs[i + 1]), xs[i + 2]))
+            i += 3
+        elif xs[i] == ASSIGN_REG_REG:
+            # print('ASSIGN_REG_REG {} {}'.format(reg_name(xs[i + 1]), reg_name(xs[i + 2])))
+            print('{} <- ${}'.format(reg_name(xs[i + 1]), reg_name(xs[i + 2])))
+            i += 3
+        elif xs[i] == ADD:
+            print('add')
+            i += 1
+        elif xs[i] == SUB:
+            print('sub')
+            i += 1
+        elif xs[i] == MUL:
+            print('mul')
+            i += 1
+        elif xs[i] == DIV:
+            print('div')
+            i += 1
+        elif xs[i] == PUSH_REG:
+            print('push ${}'.format(reg_name(xs[i + 1])))
+            i += 2
+        elif xs[i] == POP:
+            print('{} <- pop'.format(reg_name(xs[i + 1])))
+            i += 2
+        else:
+            raise Exception('Cannot print instruction: {}'.format(xs[i]))
+
+def eval(instructions):
+    print_instructions(instructions)
+    ip = 0
+    cont = True
+    while ip < len(instructions):
+        if instructions[ip] == ASSIGN_REG_LIT:
+            r = instructions[ip + 1]
+            x = instructions[ip + 2]
+            registers[r] = x
+            ip += 3
+        elif instructions[ip] == ASSIGN_REG_REG:
+            r_dst = instructions[ip + 1]
+            r_src = instructions[ip + 2]
+            registers[r_dst] = registers[r_src]
+            ip += 3
+        elif instructions[ip] == ADD:
+            registers[RES] = registers[X] + registers[Y]
+            ip += 1
+        elif instructions[ip] == MUL:
+            registers[RES] = registers[X] * registers[Y]
+            ip += 1
+        elif instructions[ip] == SUB:
+            registers[RES] = registers[X] - registers[Y]
+            ip += 1
+        elif instructions[ip] == MUL:
+            registers[RES] = registers[X] * registers[Y]
+            ip += 1
+        elif instructions[ip] == DIV:
+            registers[RES] = registers[X] / registers[Y]
+            ip += 1
+        elif instructions[ip] == SWAP:
+            r1 = instructions[ip + 1]
+            r2 = instructions[ip + 2]
+            registers[r1], registers[r2] = registers[r2], registers[r1]
+            ip += 3
+        elif instructions[ip] == RETURN:
+            ip += 1
+            cont = False
+            return registers[RES]
+        elif instructions[ip] == PUSH_REG:
+            src = instructions[ip + 1]
+            stack.append(registers[src])
+            ip += 2
+        elif instructions[ip] == POP:
+            dst = instructions[ip + 1]
+            registers[dst] = stack.pop()
+            ip += 2
+        else:
+            raise Exception('Cannot eval instruction: {}'.format(instructions[ip]))
+    return registers[RES]
+
+def main():
+    ast = ['ADD',
+           ['MUL',
+            ['MUL', ['CONST', 2], ['CONST', 3]],
+            ['DIV', ['CONST', 5], ['CONST', 5]]],
+           ['ADD',
+            ['SUB', ['CONST', 10], ['CONST', 1]],
+            ['MUL', ['CONST', 2], ['CONST', 2]]]]
+
+    print('result: {}'.format(eval(compile(ast))))
+
+main()
diff --git a/users/wpcarro/scratch/compiler/shell.nix b/users/wpcarro/scratch/compiler/shell.nix
new file mode 100644
index 000000000000..ec339eb91d98
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/shell.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.mkShell {
+  buildInputs = with pkgs; [
+    ocaml
+    ocamlPackages.utop
+    ocamlformat
+  ];
+}
diff --git a/users/wpcarro/scratch/compiler/tests.ml b/users/wpcarro/scratch/compiler/tests.ml
new file mode 100644
index 000000000000..828cbd16f090
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/tests.ml
@@ -0,0 +1,43 @@
+open Expr_parser
+open Type_parser
+open Inference
+
+type test = { input : string; expect : string; }
+(* type sub_test = { s1 : string; s2 : string; s3 : string } *)
+
+let ( let* ) = Option.bind
+
+let tests = [
+    { input = "((fn x x) 10)"; expect = "Integer"; };
+    { input = "(let f (fn x x) f)"; expect = "a -> a"; };
+]
+
+(* let sub_tests = [ *)
+(*     { *)
+(*       s1 = "{b |-> b -> Int}"; *)
+(*       s2 = "{a: Bool, b: Int, c: Bool}"; *)
+(*       s3 = "{a: Bool, b: Int -> Int, c: Bool}"; *)
+(*     } *)
+(* ] *)
+
+exception FailedAssertion
+exception TestError
+
+let main =
+  tests
+  |> List.iter (fun { input; expect } ->
+         Printf.sprintf ":t %s == %s\n" input expect |> print_string;
+         match (parse_language input, parse_input expect) with
+         | Some ast, Some expected ->
+            (match do_infer ast with
+             | Some actual ->
+                if actual != expected then
+                  begin
+                    print_type actual;
+                    raise FailedAssertion
+                  end
+                else
+                  print_string "Test passed.\n"
+             | _ -> raise TestError)
+         | _ -> raise TestError);
+  print_string "All tests pass!"
diff --git a/users/wpcarro/scratch/compiler/type_parser.ml b/users/wpcarro/scratch/compiler/type_parser.ml
new file mode 100644
index 000000000000..99cc8bbc4f4e
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/type_parser.ml
@@ -0,0 +1,104 @@
+(******************************************************************************
+ * Type Expression Language:
+ *
+ * Helpers:
+ *   symbol   -> [a-z]
+ *
+ * Core:
+ *   type     -> function
+ *   function -> ( variable | literal ) '->' type
+ *   literal  -> 'Integer' | 'Boolean'
+ *   variable -> symbol
+ ******************************************************************************)
+
+open Types
+open Prettify
+open Parser
+open Inference
+open Vec
+
+type side = LHS | RHS
+
+let ( let* ) = Option.bind
+
+let printsub (s : substitution) =
+  s |> Debug.substitution |> print_string |> print_newline
+
+let tokenize (x : string) : token vec =
+  let xs = Vec.create () in
+  let i = ref 0 in
+  while !i < String.length x do
+    match x.[!i] with
+    | ' ' -> i := !i + 1
+    | _ ->
+       let beg = !i in
+       while (!i < String.length x) && (x.[!i] != ' ') do
+         i := !i + 1
+       done;
+       Vec.append (String.sub x beg (!i - beg)) xs
+  done;
+  xs
+
+let rec parse_type (p : parser) : _type option =
+  parse_function p
+and parse_function (p : parser) : _type option =
+  match p#next with
+  | Some "->" ->
+     let* a = parse_literal p in
+     p#advance;
+     let* b = parse_type p in
+     Some (TypeArrow (a, b))
+  | _ -> parse_literal p
+and parse_literal (p : parser) : _type option =
+  match p#curr with
+  | Some "Integer" | Some "Int" -> p#advance; Some TypeInt
+  | Some "Boolean" | Some "Bool" -> p#advance; Some TypeBool
+  | Some _ -> parse_variable p
+  | None -> None
+and parse_variable (p : parser) : _type option =
+  match p#curr with
+  | Some x when String.length x = 1 -> p#advance; Some (TypeVariable x)
+  | _ -> None
+
+let print_tokens (xs : string vec) =
+  xs
+  |> Vec.map (Printf.sprintf "\"%s\"")
+  |> Vec.join ", "
+  |> Printf.sprintf "tokens: [ %s ]"
+  |> print_string 
+  |> print_newline
+
+let print_type (t : _type) =
+  t |> Debug.type' |> Printf.sprintf "type: %s" |> print_string |> print_newline
+
+let parse_input (x : string) : _type option =
+  let tokens = tokenize x in
+  print_tokens tokens;
+  parse_type (new parser tokens)
+
+(* Continually prompt until user provides a parseable type expression *)
+let rec read_type (arg : side) : _type =
+  let prompt = match arg with
+    | LHS -> "lhs> "
+    | RHS -> "rhs> " in
+  print_string prompt;
+  let x = read_line () in
+  match parse_input x with
+  | None ->
+     print_string "Failed to parse input.\n";
+     read_type arg
+  | Some ast ->
+     print_type ast;
+     ast
+
+let main =
+  while true do
+    begin
+      let lhs = read_type LHS in
+      let rhs = read_type RHS in
+      match unify lhs rhs with
+      | None ->
+         Printf.printf "Cannot unify \"%s\" with \"%s\"\n" (Debug.type' lhs) (Debug.type' rhs)
+      | Some x -> printsub x
+    end
+  done
diff --git a/users/wpcarro/scratch/compiler/types.ml b/users/wpcarro/scratch/compiler/types.ml
new file mode 100644
index 000000000000..0acd05737cdc
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/types.ml
@@ -0,0 +1,31 @@
+type literal 
+  = LiteralInt of int 
+  | LiteralBool of bool
+  | LiteralString of string
+
+(* Lambda Calculus definition *)
+type value =
+  | ValueLiteral of literal
+  | ValueVariable of string
+  | ValueFunction of string * value
+  | ValueApplication of value * value
+  | ValueVarApplication of string * value
+  | ValueBinder of string * value * value
+
+module FromString = Map.Make (String)
+
+type _type =
+  | TypeInt
+  | TypeBool
+  | TypeString
+  | TypeVariable of string
+  | TypeArrow of _type * _type
+
+type quantified_type = QuantifiedType of string list * _type
+
+type set = bool FromString.t
+type substitution = _type FromString.t
+
+type env = quantified_type FromString.t
+
+type inference = Inference of substitution * _type
diff --git a/users/wpcarro/scratch/compiler/vec.ml b/users/wpcarro/scratch/compiler/vec.ml
new file mode 100644
index 000000000000..549078c5d87a
--- /dev/null
+++ b/users/wpcarro/scratch/compiler/vec.ml
@@ -0,0 +1,127 @@
+(****************************************************************************** 
+ * Similar to Python's list
+ *
+ * - mutable
+ * - dynamically resized
+ * - O(1) read
+ * - O(1) write
+ * - O(1) append (average case)
+ *
+ ******************************************************************************)
+
+type 'a vec = {
+  mutable length: int;
+  mutable capacity: int;
+  mutable xs: 'a array;
+}
+
+(****************************************************************************** 
+ * Constructors
+ ******************************************************************************)
+
+let make (size : int) (seed : 'a) : 'a vec = { 
+  length = size;
+  capacity = size;
+  xs = Array.make size seed;
+}
+
+let create () = {
+  length = 0;
+  capacity = 0;
+  xs = [||];
+}
+
+let from_array (xs : 'a array) : 'a vec = {
+  length = Array.length xs;
+  capacity = Array.length xs;
+  xs = xs;
+}
+
+let from_list (xs : 'a list) : 'a vec = 
+  match xs with
+  | [] -> create ()
+  | y::ys -> 
+    let result = {
+      length = List.length xs;
+      capacity = List.length xs;
+      xs = Array.make (List.length xs) y;
+    } in
+    List.iteri (fun i x -> Array.set result.xs i x) xs;
+    result
+
+(****************************************************************************** 
+ * Miscellaneous
+ ******************************************************************************)
+
+let append (x : 'a) (v : 'a vec) =
+  if v.capacity = 0 then
+    begin
+      v.length <- 1;
+      v.capacity <- 1;
+      v.xs <- [|x|];
+    end
+  else if v.length = v.capacity then
+    begin
+      (* According to Wikipedia, Python uses 1.25 as the growth factor *)
+      let new_cap = v.capacity |> float_of_int |> Float.mul 1.25 |> ceil |> int_of_float in
+      let new_xs = Array.make new_cap x in
+      Array.iteri (fun i x -> Array.set new_xs i x) v.xs;
+      v.capacity <- new_cap;
+      v.xs <- new_xs;
+      Array.set v.xs v.length x;
+      v.length <- v.length + 1;
+    end
+  else
+    begin
+      Array.set v.xs v.length x;
+      v.length <- v.length + 1;
+    end
+
+let get (i : int) (v : 'a vec) : 'a option =
+  if i >= v.length then
+    None
+  else
+    Some v.xs.(i)
+
+let get_unsafe (i : int) (v : 'a vec) : 'a =
+  v.xs.(i)
+
+let set (i : int) (x : 'a) (v : 'a vec) : unit =
+  if i < v.length then
+    Array.set v.xs i x
+
+let length (v : 'a vec) : int = 
+  v.length
+
+let update (i : int) (f : 'a -> 'a) (v : 'a vec) : unit =
+  match get i v with
+  | None -> ()
+  | Some x -> set i (f x) v
+
+let iter (f : 'a -> unit) (v : 'a vec) : unit =
+  let n = ref 0 in
+  while !n < v.length do
+    f v.xs.(!n);
+    n := !n + 1;
+  done
+
+let join (sep : string) (v : string vec) : string =
+  if length v = 0 then
+    ""
+  else
+    let i = ref 1 in
+    let result = ref v.xs.(0) in
+    while !i < v.length do
+      result := !result ^ sep ^ v.xs.(!i);
+      i := !i + 1;
+    done;
+    !result
+
+let map (f : 'a -> 'b) (v : 'a vec) : 'b vec =
+  let result = create () in
+  iter (fun x -> append (f x) result) v;
+  result
+
+let append_to (dst : 'a vec) (xs : 'a vec) : unit =
+  iter (fun x -> append x dst) xs
+
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 000000000000..ec7b65dae0c3
--- /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 000000000000..8496d88c0c0c
--- /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 000000000000..7aa03e126b87
--- /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 000000000000..f4f5719f9f6b
--- /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 000000000000..d172b6cff724
--- /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 000000000000..0dfd6bb6d059
--- /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 000000000000..badd60503d90
--- /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 000000000000..2d84026a7b7d
--- /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 000000000000..c546419a3388
--- /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 000000000000..a098dfe74afa
--- /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 000000000000..35cb4392812e
--- /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 000000000000..01fd965fd540
--- /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 000000000000..dc30bb508887
--- /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 000000000000..a50f8b074e55
--- /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 000000000000..689be97a8503
--- /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 000000000000..e34a2b136ab6
--- /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 000000000000..9eddb34b2db3
--- /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 000000000000..eb5754f98210
--- /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 000000000000..3836bcd0c653
--- /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 000000000000..7e431e224962
--- /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 000000000000..127d48c1864b
--- /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 000000000000..03907f604044
--- /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 000000000000..93fdd9eed2d6
--- /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 000000000000..e2739f0f6055
--- /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 000000000000..2103a5b84f75
--- /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 000000000000..dfa5de42cc0b
--- /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 000000000000..27689ca76d04
--- /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 000000000000..bc7f7ceea562
--- /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 000000000000..0e7e97a20ca7
--- /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 000000000000..889663e058da
--- /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 000000000000..6e17baef3709
--- /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 000000000000..c72d19d4ed73
--- /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 000000000000..8291e54533d5
--- /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 000000000000..aab9671eb6d3
--- /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 000000000000..75a4b993944c
--- /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 000000000000..44ea93e1bd49
--- /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 000000000000..6dbe0fa0f3c3
--- /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 000000000000..4e3604d5bcca
--- /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 000000000000..1e67c3954f5f
--- /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 000000000000..c9d7d9d74151
--- /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 000000000000..379ba92abba8
--- /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 000000000000..cdb2846ea338
--- /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 000000000000..af13239941d0
--- /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 000000000000..0eb389c26bb9
--- /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 000000000000..0a2136a408f2
--- /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 000000000000..fc2c1ef7eebc
--- /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 000000000000..5601891a0d9b
--- /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 000000000000..d05e82d42b02
--- /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 000000000000..63da08ebf79a
--- /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 000000000000..47c0f53979c6
--- /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 000000000000..70461ddf5dac
--- /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 000000000000..b7396b20ce3f
--- /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 000000000000..5df12ebabdc7
--- /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 000000000000..bc167d975a7b
--- /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 000000000000..e08ea66b8f50
--- /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 000000000000..bdfbad24263c
--- /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 000000000000..8b4cdac1c271
--- /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 000000000000..6b042182f01c
--- /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 000000000000..ce013625030b
--- /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 000000000000..8e7b073dd8bd
--- /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 000000000000..fe295b0279ff
--- /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 000000000000..45621990ecf9
--- /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 000000000000..e9a4f0eb24d0
--- /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 000000000000..7fc174a2a9f3
--- /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 000000000000..6975dbe4d1d6
--- /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 000000000000..89a45c52ad8a
--- /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 000000000000..5c21d5167ce9
--- /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 000000000000..2116b27b0b97
--- /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 000000000000..5335e419f7ec
--- /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 000000000000..23b40793b8f1
--- /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 000000000000..f50db2838707
--- /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 000000000000..82fac171d5d1
--- /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 000000000000..7055b66af196
--- /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 000000000000..c01379295d32
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..03de0350d898
--- /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 000000000000..fd0374a9ce91
--- /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 000000000000..0e31214b830d
--- /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 000000000000..8aa8de8bb87a
--- /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 000000000000..4ed02ec30832
--- /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 000000000000..c9edc32c8856
--- /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 000000000000..98f5bb144e76
--- /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 000000000000..7bf1a484454d
--- /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 000000000000..a0b6d5dbae53
--- /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 000000000000..8ebb5cf29a4f
--- /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 000000000000..d6da1db3d313
--- /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 000000000000..2d478e0e37de
--- /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 000000000000..23d0813d1524
--- /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 000000000000..a265296ab066
--- /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 000000000000..52354f990e11
--- /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 000000000000..14e176b62aab
--- /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 000000000000..340aad9f5ce2
--- /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 000000000000..1f10668ec861
--- /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 000000000000..730b4bfdc873
--- /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 000000000000..6f7858ff4e5b
--- /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 000000000000..cb930d9ad648
--- /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 000000000000..d714dfef997f
--- /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 000000000000..033d11244ca7
--- /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 000000000000..4c5e57607d87
--- /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 000000000000..f1b02c4d2ed5
--- /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 000000000000..fdc5a8bd80ab
--- /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 000000000000..56a3c20ea05b
--- /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 000000000000..9c76da754167
--- /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 000000000000..0ac349c1f880
--- /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 000000000000..79c10c883211
--- /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 000000000000..9b6918ca37d8
--- /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 000000000000..36ace8405f71
--- /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 000000000000..a828230d0851
--- /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 000000000000..afa9706f97ba
--- /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 000000000000..df9b5015ad3a
--- /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 000000000000..7ef63a95315e
--- /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 000000000000..9d88a68069fd
--- /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 000000000000..90a2add06658
--- /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 000000000000..add59ed409cd
--- /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 000000000000..354e2dfb58b8
--- /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 000000000000..b876319b2f7a
--- /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 000000000000..4034449ef0cd
--- /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 000000000000..7031701994a7
--- /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 000000000000..a5b744f30f27
--- /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 000000000000..1ba46a754892
--- /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 000000000000..14deb66a8f65
--- /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 000000000000..e246415efd1f
--- /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 000000000000..7c491aef604e
--- /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 000000000000..3636be4d93b5
--- /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 000000000000..56032aa05c8c
--- /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 000000000000..e5b6d9c89332
--- /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 000000000000..f79a9f22b38b
--- /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 000000000000..200d1613ddcb
--- /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 000000000000..a5c6e4e6ee81
--- /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 000000000000..782678fb822c
--- /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 000000000000..0c0dce91b4dd
--- /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 000000000000..c237b8e52e2d
--- /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 000000000000..4c6d64494d95
--- /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 000000000000..7ddea5350a4f
--- /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 000000000000..80ac01dafd56
--- /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 000000000000..bbd52fa9c64c
--- /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 000000000000..688c340b987b
--- /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 000000000000..523ecd959daf
--- /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 000000000000..877bb218fdf5
--- /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 000000000000..4629798cf711
--- /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 000000000000..ced3b336e0d9
--- /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 000000000000..bfa465f98d7f
--- /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 000000000000..ae04f5eb96c0
--- /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 000000000000..dd258d924d10
--- /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 000000000000..b57f469b49d2
--- /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 000000000000..3aebd454833b
--- /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 000000000000..7761bf1c61df
--- /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 000000000000..052db44153d4
--- /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 000000000000..6e915faf2913
--- /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 000000000000..56f54d497808
--- /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 000000000000..1ae7061e8393
--- /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 000000000000..a034fb49611d
--- /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 000000000000..8a838db45d1e
--- /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 000000000000..ae9377ad116b
--- /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 000000000000..6da44572ee7e
--- /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 000000000000..8438b059d843
--- /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 000000000000..b22983ed7aff
--- /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 000000000000..69c2fc431296
--- /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 000000000000..57dfb058c037
--- /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 000000000000..ed3a7cea80e5
--- /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 000000000000..342c29be6bdb
--- /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 000000000000..de7326b05822
--- /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 000000000000..1535b5a9faac
--- /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 000000000000..81aa5cd48744
--- /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 000000000000..fc9326886cd8
--- /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 000000000000..d2fc3cf5cfcc
--- /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 000000000000..4e24983af772
--- /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 000000000000..f524067b3b44
--- /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 000000000000..b5242c4bd3a2
--- /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 000000000000..3975e973fefa
--- /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 000000000000..407bff61c980
--- /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 000000000000..7fc2ef34e2ff
--- /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 000000000000..30603578ffb3
--- /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 000000000000..0c7b7cb5a0c4
--- /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 000000000000..d381386b6257
--- /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 000000000000..a71abeb00552
--- /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 000000000000..53a47b278333
--- /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 000000000000..03b2de015dfe
--- /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 000000000000..30c95a66c319
--- /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 000000000000..e9e7f6a9c159
--- /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 000000000000..f406d64e657f
--- /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 000000000000..e23972d4186d
--- /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 000000000000..9ccc08526a99
--- /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 000000000000..5eb4a8560674
--- /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 000000000000..a6d26aa85055
--- /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 000000000000..56f2c0b27456
--- /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 000000000000..e4c61eff9f62
--- /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 000000000000..820726733f9d
--- /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 000000000000..72cd6c27a370
--- /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 000000000000..5a38b828a346
--- /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 000000000000..e772d758473d
--- /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 000000000000..2815dec9ee60
--- /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 000000000000..21a6a96c6072
--- /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 000000000000..2a843e22166b
--- /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 000000000000..7a3304bc51b9
--- /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 000000000000..8e42f8152311
--- /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 000000000000..6ac99267dbfa
--- /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 000000000000..c8a10ae5f181
--- /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 000000000000..874005a01932
--- /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 000000000000..e2565a32313d
--- /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 000000000000..9a3e8a045e88
--- /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 000000000000..88422e3631db
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..ed43c9a3e887
--- /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 000000000000..a823b2a8ebc3
--- /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 000000000000..0c6a298bf2b0
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..12aab7f08e05
--- /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 000000000000..8259606da374
--- /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 000000000000..bb1f89987e29
--- /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 000000000000..378cfb7ceae6
--- /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 000000000000..5b59d9e9ba50
--- /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 000000000000..1de8937fced4
--- /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 000000000000..3a780fc16c82
--- /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 000000000000..2f80b457b125
--- /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 000000000000..42608fb0c961
--- /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 000000000000..7cb7b4a1bbc1
--- /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 000000000000..49dbe746d364
--- /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 000000000000..f63e0ecdf11c
--- /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 000000000000..5dc4ea411bc2
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..03a49817f745
--- /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 000000000000..570a7fd5a77b
--- /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 000000000000..8c87a1ce7694
--- /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 000000000000..08395f920909
--- /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 000000000000..1c4d245811e8
--- /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 000000000000..a4c1c7ae6631
--- /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 000000000000..85d3865a51bf
--- /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 000000000000..2507208f5c1a
--- /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/rust/.gitignore b/users/wpcarro/scratch/rust/.gitignore
new file mode 100644
index 000000000000..9f970225adb6
--- /dev/null
+++ b/users/wpcarro/scratch/rust/.gitignore
@@ -0,0 +1 @@
+target/
\ No newline at end of file
diff --git a/users/wpcarro/scratch/rust/Cargo.lock b/users/wpcarro/scratch/rust/Cargo.lock
new file mode 100644
index 000000000000..28aa1250cea4
--- /dev/null
+++ b/users/wpcarro/scratch/rust/Cargo.lock
@@ -0,0 +1,89 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "itoa"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rust"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[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"
+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 = "syn"
+version = "1.0.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
diff --git a/users/wpcarro/scratch/rust/Cargo.toml b/users/wpcarro/scratch/rust/Cargo.toml
new file mode 100644
index 000000000000..76235d11d37d
--- /dev/null
+++ b/users/wpcarro/scratch/rust/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "rust"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+serde_json = "1.0.81"
+serde = { version = "1.0.137", features = ["derive"] }
diff --git a/users/wpcarro/scratch/rust/README.md b/users/wpcarro/scratch/rust/README.md
new file mode 100644
index 000000000000..9ff7dd97eaf6
--- /dev/null
+++ b/users/wpcarro/scratch/rust/README.md
@@ -0,0 +1,11 @@
+# Rust
+
+Watch me fumble around as I learn Rust.
+
+## Usage
+
+```shell
+$ nix-shell /depot -A users.wpcarro.scratch.rust
+$ cargo new json && cd ./json
+$ cargo run json
+```
diff --git a/users/wpcarro/scratch/rust/shell.nix b/users/wpcarro/scratch/rust/shell.nix
new file mode 100644
index 000000000000..98e2dbf4b29b
--- /dev/null
+++ b/users/wpcarro/scratch/rust/shell.nix
@@ -0,0 +1,7 @@
+{ pkgs ? import <nixpkgs> { }, ... }:
+
+pkgs.mkShell {
+  buildInputs = [
+    pkgs.cargo
+  ];
+}
diff --git a/users/wpcarro/scratch/rust/src/display/mod.rs b/users/wpcarro/scratch/rust/src/display/mod.rs
new file mode 100644
index 000000000000..838463109190
--- /dev/null
+++ b/users/wpcarro/scratch/rust/src/display/mod.rs
@@ -0,0 +1,13 @@
+use std::fmt;
+
+pub struct Person {
+    pub fname: String,
+    pub lname: String,
+    pub age: i8,
+}
+
+impl fmt::Display for Person {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}, {} ({} years old)", self.lname, self.fname, self.age)
+    }
+}
diff --git a/users/wpcarro/scratch/rust/src/json/mod.rs b/users/wpcarro/scratch/rust/src/json/mod.rs
new file mode 100644
index 000000000000..d3307b394ea4
--- /dev/null
+++ b/users/wpcarro/scratch/rust/src/json/mod.rs
@@ -0,0 +1,81 @@
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+
+// From the serde_json docs:
+//
+// > There are three common ways that you might find yourself needing to work
+// > with JSON data in Rust.
+// >
+// > 1. As text data. An unprocessed string of JSON data that you receive on an
+// >    HTTP endpoint, read from a file, or prepare to send to a remote server.
+// > 2. As an untyped or loosely typed representation. Maybe you want to check
+// >    that some JSON data is valid before passing it on, but without knowing
+// >    the structure of what it contains. Or you want to do very basic
+// >    manipulations like insert a key in a particular spot.
+// > 3. As a strongly typed Rust data structure. When you expect all or most of
+// >    your data to conform to a particular structure and want to get real work
+// >    done without JSON’s loosey-goosey nature tripping you up.
+//
+// So let's take a look at all three...
+
+////////////////////////////////////////////////////////////////////////////////
+// Types
+////////////////////////////////////////////////////////////////////////////////
+
+#[derive(Serialize, Deserialize, Debug)]
+struct Person {
+    fname: String,
+    lname: String,
+    age: u8,
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// Functions
+////////////////////////////////////////////////////////////////////////////////
+
+// 1) Reading/writing from/to plain text.
+//    TL;DR:
+//    - read:  serde_json::from_str(data)
+//    - write: x.to_string()
+pub fn one() {
+    let data = json!({
+        "fname": "William",
+        "lname": "Carroll",
+        "age": 30,
+    })
+    .to_string();
+
+    println!("result: {:?}", data);
+}
+
+// 2) Parse into a loosely typed representation; mutate it; serialize it back.
+//    TL;DR:
+//    - read:  serde_json::from_str(data)
+//    - write: x.to_string()
+pub fn two() {
+    let data = r#"{"fname":"William","lname":"Carroll","age":30}"#;
+
+    let mut parsed: Value = serde_json::from_str(data).unwrap();
+    parsed["fname"] = json!("Norm");
+    parsed["lname"] = json!("Macdonald");
+    parsed["age"] = json!(61);
+
+    let result = parsed.to_string();
+    println!("result: {:?}", result);
+}
+
+// 3) Parse into a strongly typed structure.
+//    TL;DR:
+//    - read:  serde_json::from_str(data)
+//    - write: serde_json::to_string(x).unwrap()
+pub fn three() {
+    let data = r#"{"fname":"William","lname":"Carroll","age":30}"#;
+
+    let mut read: Person = serde_json::from_str(data).unwrap();
+    read.fname = "Norm".to_string();
+    read.lname = "Macdonald".to_string();
+    read.age = 61;
+
+    let write = serde_json::to_string(&read).unwrap();
+    println!("result: {:?}", write);
+}
diff --git a/users/wpcarro/scratch/rust/src/main.rs b/users/wpcarro/scratch/rust/src/main.rs
new file mode 100644
index 000000000000..671b33093050
--- /dev/null
+++ b/users/wpcarro/scratch/rust/src/main.rs
@@ -0,0 +1,15 @@
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Value};
+
+mod display;
+mod json;
+mod rc;
+mod stdin;
+
+////////////////////////////////////////////////////////////////////////////////
+// Main
+////////////////////////////////////////////////////////////////////////////////
+
+fn main() {
+    rc::example();
+}
diff --git a/users/wpcarro/scratch/rust/src/rc/mod.rs b/users/wpcarro/scratch/rust/src/rc/mod.rs
new file mode 100644
index 000000000000..67251ca6aa9b
--- /dev/null
+++ b/users/wpcarro/scratch/rust/src/rc/mod.rs
@@ -0,0 +1,12 @@
+// Playing around with Rust's "smart pointers". Starting off with a wrapper type
+// that allows multiple readers (owners?) of some data.
+
+use std::rc::Rc;
+
+pub fn example() {
+    let five = Rc::new(5);
+    let x = Rc::clone(&five);
+    let y = Rc::clone(&five);
+    let z = Rc::clone(&five);
+    println!("result: {}", *x + *y + *z)
+}
diff --git a/users/wpcarro/scratch/rust/src/stdin/mod.rs b/users/wpcarro/scratch/rust/src/stdin/mod.rs
new file mode 100644
index 000000000000..4be95afa4547
--- /dev/null
+++ b/users/wpcarro/scratch/rust/src/stdin/mod.rs
@@ -0,0 +1,22 @@
+use std::io::Write;
+use std::process::{Command, Stdio};
+
+// Example of piping-in a string defined in Rust to a shell command.
+pub fn example() {
+    let input = "Hello, world!";
+
+    let mut cat = Command::new("cat")
+        .stdin(Stdio::piped())
+        .spawn()
+        .ok()
+        .unwrap();
+
+    cat.stdin
+        .take()
+        .unwrap()
+        .write_all(&input.as_bytes())
+        .unwrap();
+
+    let output = cat.wait_with_output().unwrap();
+    println!("{}", String::from_utf8_lossy(&output.stdout));
+}
diff --git a/users/wpcarro/scratch/simple-select/README.md b/users/wpcarro/scratch/simple-select/README.md
new file mode 100644
index 000000000000..69e5707302ab
--- /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 000000000000..3ae6c5d60e08
--- /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 000000000000..d26f970e57a2
--- /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 000000000000..5dae68aee551
--- /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 000000000000..d4c02bf69365
--- /dev/null
+++ b/users/wpcarro/secrets.json.secret
Binary files differdiff --git a/users/wpcarro/slx.js/.gitignore b/users/wpcarro/slx.js/.gitignore
new file mode 100644
index 000000000000..d60e5798c108
--- /dev/null
+++ b/users/wpcarro/slx.js/.gitignore
@@ -0,0 +1,3 @@
+/.parcel-cache
+/dist
+/node_modules
\ No newline at end of file
diff --git a/users/wpcarro/slx.js/README.md b/users/wpcarro/slx.js/README.md
new file mode 100644
index 000000000000..3fbebc470633
--- /dev/null
+++ b/users/wpcarro/slx.js/README.md
@@ -0,0 +1,55 @@
+# slx.js
+
+Filter tabular data in the browser using an ergonomic query language.
+
+## Status
+
+This project is usable today (I use it in my projects), but it's currently alpha
+status. See the wish list for remaining features.
+
+## Installation
+
+`slx.js` is available via CDN:
+
+```shell
+<script src="https://cdn.jsdelivr.net/gh/wpcarro/slx.js/index.js" async></script>
+```
+
+## Usage
+
+`slx.js` hasn't been properly benchmarked, but in my personal projects, it works
+fine with `O(1,000)s` of records.
+
+```javascript
+const cast = [
+  { first: "Graham", last: "Chapman" },
+  { first: "John", last: "Cleese" },
+  { first: "Terry", last: "Gilliam" },
+  { first: "Eric", last: "Idle" },
+  { first: "Terry", last: "Jones" },
+  { first: "Michael", last: "Palin" },
+];
+
+const config = {
+    // Match values case sensitively when filtering.
+    caseSensitive: false,
+    // Coerce values into regular expressions (instead of strings) when they're defined as atoms.
+    preferRegex: true,
+    // The key in the JS object that hosts the Date type against which we filter.
+    dateKey: 'Date',
+};
+
+console.log(select('last:^C.+$', cast, config));
+// [{ first: "Graham", last: "Chapman" }, { first: "John", last: "Cleese" }]
+```
+
+## Wish List
+
+- Support explicit grouping with parentheses (e.g. `title:once (director:Tarantino OR director:Coen)`).
+- Proper benchmarking (see "Usage" section).
+- Something something documentation.
+- Something something testing.
+
+## See also:
+
+- [`slx`](https://github.com/wpcarro/slx)
diff --git a/users/wpcarro/slx.js/default.nix b/users/wpcarro/slx.js/default.nix
new file mode 100644
index 000000000000..bf903e77aadd
--- /dev/null
+++ b/users/wpcarro/slx.js/default.nix
@@ -0,0 +1,11 @@
+{ pkgs, depot, ... }:
+
+(pkgs.writeText "source.txt" ''
+  ${depot.third_party.gitignoreSource ./.}
+'').overrideAttrs (_: {
+  meta.ci.extraSteps.github = depot.tools.releases.filteredGitPush {
+    filter = ":/users/wpcarro/slx.js";
+    remote = "git@github.com:wpcarro/slx.js.git";
+    ref = "refs/heads/canon";
+  };
+})
diff --git a/users/wpcarro/slx.js/index.html b/users/wpcarro/slx.js/index.html
new file mode 100644
index 000000000000..966705a6427e
--- /dev/null
+++ b/users/wpcarro/slx.js/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <title>Tests</title>
+    <link rel="stylesheet" href="https://unpkg.com/terminal.css@0.7.2/dist/terminal.min.css" />
+  </head>
+  <body>
+    <div id="mount"></div>
+    <script src="./index.js"></script>
+    <script src="./tests.js" type="module"></script>
+  </body>
+</html>
diff --git a/users/wpcarro/slx.js/index.js b/users/wpcarro/slx.js/index.js
new file mode 100644
index 000000000000..3729978c75ef
--- /dev/null
+++ b/users/wpcarro/slx.js/index.js
@@ -0,0 +1,455 @@
+function select(query, xs, config) {
+    // naive optimizations
+    if (query === '' || xs === []) {
+        return xs;
+    }
+
+    const predicate = compile(parse(query, config), config);
+    return xs.filter(predicate);
+}
+
+function compile(ast, config) {
+    if (ast.type === 'CONJUNCTION') {
+        const lhs = compile(ast.lhs, compile);
+        const rhs = compile(ast.rhs, compile);
+
+        if (ast.joint === 'AND') {
+            return function(x) {
+                return lhs(x) && rhs(x);
+            };
+        }
+        if (ast.joint === 'OR') {
+            return function(x) {
+                return lhs(x) || rhs(x);
+            };
+        }
+    }
+    if (ast.type === 'DATE_SELECTION') {
+        if (ast.key === 'before') {
+            return function(row) {
+                let t = new Date();
+                if (ast.val === 'yesterday') {
+                    t.setDate(t.getDate() - 1);
+                    console.log(t);
+                }
+                // MM/DD/YYYY
+                else {
+                    t = new Date(ast.val);
+                }
+                return row[config.dateKey] < t;
+            };
+        }
+        if (ast.key === 'after') {
+            return function(row) {
+                let t = new Date();
+                if (ast.val === 'yesterday') {
+                    t.setDate(t.getDate() - 1);
+                    console.log(t);
+                }
+                // MM/DD/YYYY
+                else {
+                    t = new Date(ast.val);
+                }
+                return row[config.dateKey] > t;
+            };
+        }
+    }
+    if (ast.type === 'COMPARE_SELECTION') {
+        const f = compile(ast.val, config);
+
+        let compare = null;
+        if (ast.operator === 'EQ') { compare = (x, y) => x === y; }
+        if (ast.operator === 'LT') { compare = (x, y) => x < y; }
+        if (ast.operator === 'GT') { compare = (x, y) => x > y; }
+        if (ast.operator === 'LTE') { compare = (x, y) => x <= y; }
+        if (ast.operator === 'GTE') { compare = (x, y) => x >= y; }
+
+        return function(row) {
+            return ast.negate ? !compare(row[ast.key], ast.val) : compare(row[ast.key], ast.val);
+        };
+    }
+    if (ast.type === 'SELECTION') {
+        const f = compile(ast.val, config);
+        return function(row) {
+            return ast.negate ? !f(row[ast.key]) : f(row[ast.key]);
+        };
+    }
+    if (ast.type === 'MATCH_ALL') {
+        if (ast.matchType === 'STRING') {
+            return function(row) {
+                return Object.values(row).some(x => {
+                    if (config.caseSensitive) {
+                        return x === ast.val;
+                    } else {
+                        return x.toLowerCase() === ast.val.toLowerCase();
+                    }
+                })
+            };
+        }
+        if (ast.matchType === 'REGEX') {
+            return function(row) {
+                return Object.values(row).some(x => ast.val.test(x));
+            };
+        }
+    }
+    if (ast.type === 'GROUPING') {
+        return compile(ast.content);
+    }
+    if (ast.type === 'STRING') {
+        return function(x) {
+            if (config.caseSensitive) {
+                return x === ast.val;
+            } else {
+                return x.toLowerCase() === ast.val.toLowerCase();
+            }
+        };
+    }
+    if (ast.type === 'REGEX') {
+        return function(x) {
+            return ast.val.test(x);
+        };
+    }
+}
+
+// A "selection" without a "$column:" prefix should fuzzy-search all columns.
+//
+// conjunction -> selection ( ( "AND" | "OR" )? selection )* ;
+// selection   -> "-"? COLUMN ":" ( regex | string ) | regex ;
+// regex       -> [_-a-zA-Z0-9] | "/" [ _-a-zA-Z0-9] "/" | string ;
+// string      -> "\"" [ _-a-zA-Z0-9] "\"" ;
+
+// Whatever characters are valid for a JS regex.
+const ATOM_REGEX = /[-_.\[\]a-zA-Z0-9*+^$]/;
+
+function tokenize(x) {
+    const result = [];
+    let i = 0;
+    while (i < x.length) {
+        if (x[i] === ' ') {
+            i += 1;
+            while (i < x.length && x[i] === ' ') {
+                i += 1;
+            }
+            result.push(['WHITESPACE', null]);
+            continue;
+        }
+        if (x[i] === '-') {
+            result.push(['NEGATE', null]);
+            i += 1;
+            continue;
+        }
+        // Tokenize numbers (i.e. integers, floats).
+        if (/[0-9]/.test(x[i])) {
+            let curr = x[i];
+            i += 1;
+            while (i < x.length && /[0-9]/.test(x[i])) {
+                curr += x[i];
+                i += 1;
+            }
+            result.push(['NUMBER', parseFloat(curr)]);
+            continue;
+        }
+        if (ATOM_REGEX.test(x[i])) {
+            let curr = x[i];
+            i += 1;
+            while (i < x.length && ATOM_REGEX.test(x[i])) {
+                curr += x[i];
+                i += 1;
+            }
+            result.push(['ATOM', curr]);
+            continue;
+        }
+        if (x[i] === '=') {
+            result.push(['COMPARE', 'EQ']);
+            i += 1;
+            continue;
+        }
+        if (x[i] === '<' && i + 1 < x.length && x[i + 1] === '=') {
+            result.push(['COMPARE', 'LTE']);
+            i += 2;
+            continue;
+        }
+        if (x[i] === '<') {
+            result.push(['COMPARE', 'LT']);
+            i += 1;
+            continue;
+        }
+        if (x[i] === '>' && i + i < x.length && x[i + 1] === '=') {
+            result.push(['COMPARE', 'GTE']);
+            i += 2;
+            continue;
+        }
+        if (x[i] === '>') {
+            result.push(['COMPARE', 'GT']);
+            i += 1;
+            continue;
+        }
+        if (x[i] === ':') {
+            result.push(['COLON', null]);
+            i += 1;
+            continue;
+        }
+        if (x[i] === '(') {
+            result.push(['LPAREN', null]);
+            i += 1;
+            continue;
+        }
+        if (x[i] === ')') {
+            result.push(['RPAREN', null]);
+            i += 1;
+            continue;
+        }
+        if (x[i] === '/') {
+            let start = i;
+            let curr = '';
+            i += 1;
+            while (i < x.length && x[i] !== '/') {
+                curr += x[i];
+                i += 1;
+            }
+            // error
+            if (i >= x.length) {
+                throw `Tokenize Error: EOL while attempting to tokenize the regex beginning at column: ${start}`;
+            }
+            if (x[i] === '/') {
+                result.push(['REGEX', curr]);
+                i += 1;
+            }
+            continue;
+        }
+        if (x[i] === '"') {
+            let start = i;
+            let curr = '';
+            i += 1;
+            while (i < x.length && x[i] !== '"') {
+                // continue on \"
+                if (x[i] === '\\' && x[i + 1] === '"') {
+                    curr += '\"';
+                    i += 2;
+                } else {
+                    curr += x[i];
+                    i += 1;
+                }
+            }
+            if (i >= x.length) {
+                throw `Tokenize Error: EOL while attempting to tokenize the string starting at column: ${start}`;
+            }
+            if (x[i] === '"') {
+                result.push(['STRING', curr]);
+                i += 1;
+            }
+            continue;
+        }
+        else {
+            i += 1;
+        }
+    }
+    return result;
+}
+
+function expect(f, expectation, p) {
+    const [type, val] = p.tokens[p.i];
+    if (f(type, val)) {
+        p.i += 1;
+    } else {
+        throw `Parse Error: expected ${expectation}, but got ${p.tokens[p.i]}; ${JSON.stringify(p)}`
+    }
+}
+
+function matches(f, p) {
+    const [type, val] = p.tokens[p.i];
+    if (f(type, val)) {
+        return true;
+    }
+    return false;
+}
+
+function match(f, expectation, p) {
+    const [type, val] = p.tokens[p.i];
+    if (f(type, val)) {
+        p.i += 1;
+        return val;
+    }
+    throw `Parse Error: expected ${expectation}, but got: ${p.tokens[p.i]}; ${JSON.stringify(p)}`;
+}
+
+function skipWhitespace(p) {
+    while (p.i < p.tokens.length && matches((type, _) => type === 'WHITESPACE', p)) {
+        p.i += 1;
+    }
+}
+
+function peekType(n, p) {
+    if (p.i + n < p.tokens.length) {
+        return p.tokens[p.i + n][0];
+    }
+    return null;
+}
+
+function parser(tokens) {
+    return { i: 0, tokens };
+}
+
+function parse(x, config) {
+    const tokens = tokenize(x);
+    const p = parser(tokens);
+    return conjunction(p, config);
+}
+
+function conjunction(p, config) {
+    skipWhitespace(p);
+
+    const lhs = selection(p, config);
+    skipWhitespace(p);
+
+    // TODO(wpcarro): Consider re-architecting the parser to avoid smells like
+    // this.
+    if (peekType(0, p) === 'RPAREN') {
+        return lhs;
+    }
+
+    if (p.i >= p.tokens.length) {
+        return lhs;
+    }
+
+    let joint = 'AND';
+    if (matches((type, val) => type === 'ATOM' && val === 'AND', p)) {
+        joint = 'AND';
+        p.i += 1;
+    } else if (matches((type, val) => type === 'ATOM' && val === 'OR', p)) {
+        joint = 'OR';
+        p.i += 1;
+    }
+    skipWhitespace(p);
+    const rhs = conjunction(p, config);
+
+    return {
+        type: 'CONJUNCTION',
+        joint,
+        lhs,
+        rhs,
+    };
+}
+
+function selection(p, config) {
+    // column:value OR -column:value
+    if ((peekType(0, p) === 'ATOM' && peekType(1, p) === 'COLON') ||
+        (peekType(0, p) === 'NEGATE' && peekType(1, p) === 'ATOM' && peekType(2, p) === 'COLON')) {
+
+        let negate = false;
+        if (p.tokens[p.i][0] === 'NEGATE') {
+            negate = true;
+            p.i += 1;
+        }
+
+        const key = match((type, _) => type === 'ATOM', 'a column label', p);
+        expect((type, val) => type === 'COLON', 'a colon', p);
+
+        if (key === 'before' || key === 'after') {
+            const val = date(p);
+            return {
+                type: 'DATE_SELECTION',
+                key,
+                val,
+            };
+        } else {
+            const val = value(p, config);
+            return {
+                type: 'SELECTION',
+                negate,
+                key,
+                val,
+            };
+        }
+    }
+    // column<value OR -column<value
+    else if ((peekType(0, p) === 'ATOM' && peekType(1, p) === 'COMPARE') ||
+             (peekType(0, p) === 'NEGATE' && peekType(1, p) === 'ATOM' && peekType(2, p) === 'COMPARE')) {
+        let negate = false;
+        if (p.tokens[p.i][0] === 'NEGATE') {
+            negate = true;
+            p.i += 1;
+        }
+
+        const key = match((type, _) => type === 'ATOM', 'a column label', p);
+        const operator = match((type, _) => type === 'COMPARE', 'a comparison operator (i.e. "<", ">", "<=", ">=")', p);
+        const val = match((type, _) => type === 'NUMBER', 'a number', p);
+
+        return {
+            type: 'COMPARE_SELECTION',
+            operator,
+            negate,
+            key,
+            val,
+        };
+    }
+    else {
+        return matchAll(p, config);
+    }
+}
+
+function matchAll(p, config) {
+    const [type, val] = p.tokens[p.i];
+
+    // Cast atoms into strings or regexes depending on the current config.
+    if (type === 'ATOM') {
+        p.i += 1;
+        if (config.preferRegex) {
+            const regex = config.caseSensitive ? new RegExp(val) : new RegExp(val, "i");
+            return { type: 'MATCH_ALL', matchType: 'REGEX', val: regex };
+        } else {
+            return { type: 'MATCH_ALL', matchType: 'STRING', val }
+        }
+    }
+    if (type === 'STRING') {
+        p.i += 1;
+        return { type: 'MATCH_ALL', matchType: 'STRING', val };
+    }
+    if (type === 'REGEX') {
+        p.i += 1;
+        const regex = config.caseSensitive ? new RegExp(val) : new RegExp(val, "i");
+        return { type: 'MATCH_ALL', matchType: 'REGEX', val: regex };
+    }
+    if (type === 'LPAREN') {
+        p.i += 1;
+        const content = conjunction(p, config);
+        expect((type, _) => type === 'RPAREN', 'a closing parenthesis', p);
+        return {
+            type: 'GROUPING',
+            content,
+        };
+    }
+    throw `Parse Error: Expected a regular expression or a string, but got: ${p.tokens[p.i]}; ${JSON.stringify(p)}`;
+}
+
+function value(p, config) {
+    const [type, val] = p.tokens[p.i];
+
+    // Cast atoms into strings or regexes depending on the current config.
+    if (type === 'ATOM') {
+        p.i += 1;
+        if (config.preferRegex) {
+            const regex = config.caseSensitive ? new RegExp(val) : new RegExp(val, "i");
+            return { type: 'REGEX', val: regex };
+        } else {
+            return { type: 'STRING', val }
+        }
+    }
+    if (type === 'STRING') {
+        p.i += 1;
+        return { type, val };
+    }
+    if (type === 'REGEX') {
+        p.i += 1;
+        const regex = config.caseSensitive ? new RegExp(val) : new RegExp(val, "i");
+        return { type, val: regex };
+    }
+    throw `Parse Error: Expected a regular expression or a string, but got: ${p.tokens[p.i]}; ${JSON.stringify(p)}`;
+}
+
+function date(p) {
+    const [type, val] = p.tokens[p.i];
+    p.i += 1;
+
+    return val;
+}
diff --git a/users/wpcarro/slx.js/package.json b/users/wpcarro/slx.js/package.json
new file mode 100644
index 000000000000..d8f2e678faca
--- /dev/null
+++ b/users/wpcarro/slx.js/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "slx.js",
+  "version": "1.0.0",
+  "main": "index.js",
+  "license": "MIT",
+  "dependencies": {
+    "parcel": "^2.8.3",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
+  },
+  "devDependencies": {
+    "process": "^0.11.10"
+  }
+}
diff --git a/users/wpcarro/slx.js/tests.js b/users/wpcarro/slx.js/tests.js
new file mode 100644
index 000000000000..9ed68a588c55
--- /dev/null
+++ b/users/wpcarro/slx.js/tests.js
@@ -0,0 +1,68 @@
+import { createRoot } from "react-dom/client";
+import React from "react";
+
+
+const john = { first: 'John', last: 'Cleese', age: 83, birthday: new Date("10/27/1939") };
+const graham = { first: 'Graham', last: 'Chapman', age: 48, birthday: new Date("01/08/1941") };
+
+const xs = [
+    john,
+    graham,
+];
+const cfg = {
+    caseSensitive: false,
+    preferRegex: true,
+    dateKey: 'birthday',
+};
+const tests = [
+    ['support EQ', 'age=83', xs, cfg, [john]],
+    ['supports LT', 'age<83', xs, cfg, [graham]],
+    ['supports LTE', 'age<=83', xs, cfg, [john, graham]],
+    ['supports GT', 'age>48', xs, cfg, [john]],
+    ['supports GTE', 'age>=48', xs, cfg, [john, graham]],
+    ['supports grouping (1)', 'last:/^C/ (age=83 OR age=48)', xs, cfg, [john, graham]],
+    ['supports grouping (2)', '(age=83)', xs, cfg, [john]],
+    ['supports grouping (3)', '(age=83 OR age=48)', xs, cfg, [john, graham]],
+];
+
+function equal(xs, ys) {
+    return xs.length === ys.length && xs.every((x, i) => x === ys[i]);
+}
+
+class App extends React.Component {
+    constructor(props) {
+        super(props);
+    }
+    render() {
+        return (
+            <table>
+              <thead>
+                <th>pass/fail</th>
+                <th>Label</th>
+                <th>code</th>
+                <th>actual</th>
+                <th>expected</th>
+              </thead>
+              <tbody>
+                {this.props.tests.map(test => {
+                    const [label, query, xs, cfg, expected] = test;
+                    const actual = select(query, xs, cfg);
+                    return (
+                        <tr style={{backgroundColor: equal(actual, expected) ? null : 'red'}}>
+                          <td>{equal(actual, expected) ? "pass" : "fail"}</td>
+                          <td>{label}</td>
+                          <td>select("{query}", {JSON.stringify(xs)}, {JSON.stringify(cfg)})</td>
+                          <td>{JSON.stringify(actual)}</td>
+                          <td>{JSON.stringify(expected)}</td>
+                        </tr>
+                    );
+                })}
+              </tbody>
+            </table>
+        );
+    }
+}
+
+const container = document.getElementById("mount");
+const root = createRoot(container);
+root.render(<App tests={tests} />);
diff --git a/users/wpcarro/slx.js/yarn.lock b/users/wpcarro/slx.js/yarn.lock
new file mode 100644
index 000000000000..4d0ec7633f86
--- /dev/null
+++ b/users/wpcarro/slx.js/yarn.lock
@@ -0,0 +1,1495 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
+  integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
+  dependencies:
+    "@babel/highlight" "^7.18.6"
+
+"@babel/helper-validator-identifier@^7.18.6":
+  version "7.19.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
+  integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
+
+"@babel/highlight@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
+  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.18.6"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@jridgewell/gen-mapping@^0.3.0":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
+  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/set-array@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
+  integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
+  version "1.4.14"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.17"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
+  integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
+  dependencies:
+    "@jridgewell/resolve-uri" "3.1.0"
+    "@jridgewell/sourcemap-codec" "1.4.14"
+
+"@lezer/common@^0.15.0", "@lezer/common@^0.15.7":
+  version "0.15.12"
+  resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.12.tgz#2f21aec551dd5fd7d24eb069f90f54d5bc6ee5e9"
+  integrity sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig==
+
+"@lezer/lr@^0.15.4":
+  version "0.15.8"
+  resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-0.15.8.tgz#1564a911e62b0a0f75ca63794a6aa8c5dc63db21"
+  integrity sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg==
+  dependencies:
+    "@lezer/common" "^0.15.0"
+
+"@lmdb/lmdb-darwin-arm64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.5.2.tgz#bc66fa43286b5c082e8fee0eacc17995806b6fbe"
+  integrity sha512-+F8ioQIUN68B4UFiIBYu0QQvgb9FmlKw2ctQMSBfW2QBrZIxz9vD9jCGqTCPqZBRbPHAS/vG1zSXnKqnS2ch/A==
+
+"@lmdb/lmdb-darwin-x64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.5.2.tgz#89d8390041bce6bab24a82a20392be22faf54ffc"
+  integrity sha512-KvPH56KRLLx4KSfKBx0m1r7GGGUMXm0jrKmNE7plbHlesZMuPJICtn07HYgQhj1LNsK7Yqwuvnqh1QxhJnF1EA==
+
+"@lmdb/lmdb-linux-arm64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.5.2.tgz#14fe4c96c2bb1285f93797f45915fa35ee047268"
+  integrity sha512-aLl89VHL/wjhievEOlPocoefUyWdvzVrcQ/MHQYZm2JfV1jUsrbr/ZfkPPUFvZBf+VSE+Q0clWs9l29PCX1hTQ==
+
+"@lmdb/lmdb-linux-arm@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.5.2.tgz#05bde4573ab10cf21827339fe687148f2590cfa1"
+  integrity sha512-5kQAP21hAkfW5Bl+e0P57dV4dGYnkNIpR7f/GAh6QHlgXx+vp/teVj4PGRZaKAvt0GX6++N6hF8NnGElLDuIDw==
+
+"@lmdb/lmdb-linux-x64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.5.2.tgz#d2f85afd857d2c33d2caa5b057944574edafcfee"
+  integrity sha512-xUdUfwDJLGjOUPH3BuPBt0NlIrR7f/QHKgu3GZIXswMMIihAekj2i97oI0iWG5Bok/b+OBjHPfa8IU9velnP/Q==
+
+"@lmdb/lmdb-win32-x64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.5.2.tgz#28f643fbc0bec30b07fbe95b137879b6b4d1c9c5"
+  integrity sha512-zrBczSbXKxEyK2ijtbRdICDygRqWSRPpZMN5dD1T8VMEW5RIhIbwFWw2phDRXuBQdVDpSjalCIUMWMV2h3JaZA==
+
+"@mischnic/json-sourcemap@^0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz#38af657be4108140a548638267d02a2ea3336507"
+  integrity sha512-dQb3QnfNqmQNYA4nFSN/uLaByIic58gOXq4Y4XqLOWmOrw73KmJPt/HLyG0wvn1bnR6mBKs/Uwvkh+Hns1T0XA==
+  dependencies:
+    "@lezer/common" "^0.15.7"
+    "@lezer/lr" "^0.15.4"
+    json5 "^2.2.1"
+
+"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.2.0.tgz#901c5937e1441572ea23e631fe6deca68482fe76"
+  integrity sha512-Z9LFPzfoJi4mflGWV+rv7o7ZbMU5oAU9VmzCgL240KnqDW65Y2HFCT3MW06/ITJSnbVLacmcEJA8phywK7JinQ==
+
+"@msgpackr-extract/msgpackr-extract-darwin-x64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.2.0.tgz#fb877fe6bae3c4d3cea29786737840e2ae689066"
+  integrity sha512-vq0tT8sjZsy4JdSqmadWVw6f66UXqUCabLmUVHZwUFzMgtgoIIQjT4VVRHKvlof3P/dMCkbMJ5hB1oJ9OWHaaw==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.2.0.tgz#986179c38b10ac41fbdaf7d036c825cbc72855d9"
+  integrity sha512-hlxxLdRmPyq16QCutUtP8Tm6RDWcyaLsRssaHROatgnkOxdleMTgetf9JsdncL8vLh7FVy/RN9i3XR5dnb9cRA==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.2.0.tgz#15f2c6fe9e0adc06c21af7e95f484ff4880d79ce"
+  integrity sha512-SaJ3Qq4lX9Syd2xEo9u3qPxi/OB+5JO/ngJKK97XDpa1C587H9EWYO6KD8995DAjSinWvdHKRrCOXVUC5fvGOg==
+
+"@msgpackr-extract/msgpackr-extract-linux-x64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.2.0.tgz#30cae5c9a202f3e1fa1deb3191b18ffcb2f239a2"
+  integrity sha512-94y5PJrSOqUNcFKmOl7z319FelCLAE0rz/jPCWS+UtdMZvpa4jrQd+cJPQCLp2Fes1yAW/YUQj/Di6YVT3c3Iw==
+
+"@msgpackr-extract/msgpackr-extract-win32-x64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.2.0.tgz#016d855b6bc459fd908095811f6826e45dd4ba64"
+  integrity sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA==
+
+"@parcel/bundler-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.8.3.tgz#d64739dbc2dbd59d6629861bf77a8083aced5229"
+  integrity sha512-yJvRsNWWu5fVydsWk3O2L4yIy3UZiKWO2cPDukGOIWMgp/Vbpp+2Ct5IygVRtE22bnseW/E/oe0PV3d2IkEJGg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/graph" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/cache@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.8.3.tgz#169e130cf59913c0ed9fadce1a450e68f710e16f"
+  integrity sha512-k7xv5vSQrJLdXuglo+Hv3yF4BCSs1tQ/8Vbd6CHTkOhf7LcGg6CPtLw053R/KdMpd/4GPn0QrAsOLdATm1ELtQ==
+  dependencies:
+    "@parcel/fs" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    lmdb "2.5.2"
+
+"@parcel/codeframe@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.8.3.tgz#84fb529ef70def7f5bc64f6c59b18d24826f5fcc"
+  integrity sha512-FE7sY53D6n/+2Pgg6M9iuEC6F5fvmyBkRE4d9VdnOoxhTXtkEqpqYgX7RJ12FAQwNlxKq4suBJQMgQHMF2Kjeg==
+  dependencies:
+    chalk "^4.1.0"
+
+"@parcel/compressor-raw@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/compressor-raw/-/compressor-raw-2.8.3.tgz#301753df8c6de967553149639e8a4179b88f0c95"
+  integrity sha512-bVDsqleBUxRdKMakWSlWC9ZjOcqDKE60BE+Gh3JSN6WJrycJ02P5wxjTVF4CStNP/G7X17U+nkENxSlMG77ySg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/config-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.8.3.tgz#9a43486e7c702e96c68052c37b79098d7240e35b"
+  integrity sha512-o/A/mbrO6X/BfGS65Sib8d6SSG45NYrNooNBkH/o7zbOBSRQxwyTlysleK1/3Wa35YpvFyLOwgfakqCtbGy4fw==
+  dependencies:
+    "@parcel/bundler-default" "2.8.3"
+    "@parcel/compressor-raw" "2.8.3"
+    "@parcel/namer-default" "2.8.3"
+    "@parcel/optimizer-css" "2.8.3"
+    "@parcel/optimizer-htmlnano" "2.8.3"
+    "@parcel/optimizer-image" "2.8.3"
+    "@parcel/optimizer-svgo" "2.8.3"
+    "@parcel/optimizer-terser" "2.8.3"
+    "@parcel/packager-css" "2.8.3"
+    "@parcel/packager-html" "2.8.3"
+    "@parcel/packager-js" "2.8.3"
+    "@parcel/packager-raw" "2.8.3"
+    "@parcel/packager-svg" "2.8.3"
+    "@parcel/reporter-dev-server" "2.8.3"
+    "@parcel/resolver-default" "2.8.3"
+    "@parcel/runtime-browser-hmr" "2.8.3"
+    "@parcel/runtime-js" "2.8.3"
+    "@parcel/runtime-react-refresh" "2.8.3"
+    "@parcel/runtime-service-worker" "2.8.3"
+    "@parcel/transformer-babel" "2.8.3"
+    "@parcel/transformer-css" "2.8.3"
+    "@parcel/transformer-html" "2.8.3"
+    "@parcel/transformer-image" "2.8.3"
+    "@parcel/transformer-js" "2.8.3"
+    "@parcel/transformer-json" "2.8.3"
+    "@parcel/transformer-postcss" "2.8.3"
+    "@parcel/transformer-posthtml" "2.8.3"
+    "@parcel/transformer-raw" "2.8.3"
+    "@parcel/transformer-react-refresh-wrap" "2.8.3"
+    "@parcel/transformer-svg" "2.8.3"
+
+"@parcel/core@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.8.3.tgz#22a69f36095d53736ab10bf42697d9aa5f4e382b"
+  integrity sha512-Euf/un4ZAiClnlUXqPB9phQlKbveU+2CotZv7m7i+qkgvFn5nAGnrV4h1OzQU42j9dpgOxWi7AttUDMrvkbhCQ==
+  dependencies:
+    "@mischnic/json-sourcemap" "^0.1.0"
+    "@parcel/cache" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/events" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/graph" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/package-manager" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    abortcontroller-polyfill "^1.1.9"
+    base-x "^3.0.8"
+    browserslist "^4.6.6"
+    clone "^2.1.1"
+    dotenv "^7.0.0"
+    dotenv-expand "^5.1.0"
+    json5 "^2.2.0"
+    msgpackr "^1.5.4"
+    nullthrows "^1.1.1"
+    semver "^5.7.1"
+
+"@parcel/diagnostic@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.8.3.tgz#d560276d5d2804b48beafa1feaf3fc6b2ac5e39d"
+  integrity sha512-u7wSzuMhLGWZjVNYJZq/SOViS3uFG0xwIcqXw12w54Uozd6BH8JlhVtVyAsq9kqnn7YFkw6pXHqAo5Tzh4FqsQ==
+  dependencies:
+    "@mischnic/json-sourcemap" "^0.1.0"
+    nullthrows "^1.1.1"
+
+"@parcel/events@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.8.3.tgz#205f8d874e6ecc2cbdb941bf8d54bae669e571af"
+  integrity sha512-hoIS4tAxWp8FJk3628bsgKxEvR7bq2scCVYHSqZ4fTi/s0+VymEATrRCUqf+12e5H47uw1/ZjoqrGtBI02pz4w==
+
+"@parcel/fs-search@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/fs-search/-/fs-search-2.8.3.tgz#1c7d812c110b808758f44c56e61dfffdb09e9451"
+  integrity sha512-DJBT2N8knfN7Na6PP2mett3spQLTqxFrvl0gv+TJRp61T8Ljc4VuUTb0hqBj+belaASIp3Q+e8+SgaFQu7wLiQ==
+  dependencies:
+    detect-libc "^1.0.3"
+
+"@parcel/fs@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.8.3.tgz#80536afe877fc8a2bd26be5576b9ba27bb4c5754"
+  integrity sha512-y+i+oXbT7lP0e0pJZi/YSm1vg0LDsbycFuHZIL80pNwdEppUAtibfJZCp606B7HOjMAlNZOBo48e3hPG3d8jgQ==
+  dependencies:
+    "@parcel/fs-search" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/watcher" "^2.0.7"
+    "@parcel/workers" "2.8.3"
+
+"@parcel/graph@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/graph/-/graph-2.8.3.tgz#00ffe8ec032e74fee57199e54529f1da7322571d"
+  integrity sha512-26GL8fYZPdsRhSXCZ0ZWliloK6DHlMJPWh6Z+3VVZ5mnDSbYg/rRKWmrkhnr99ZWmL9rJsv4G74ZwvDEXTMPBg==
+  dependencies:
+    nullthrows "^1.1.1"
+
+"@parcel/hash@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/hash/-/hash-2.8.3.tgz#bc2499a27395169616cad2a99e19e69b9098f6e9"
+  integrity sha512-FVItqzjWmnyP4ZsVgX+G00+6U2IzOvqDtdwQIWisCcVoXJFCqZJDy6oa2qDDFz96xCCCynjRjPdQx2jYBCpfYw==
+  dependencies:
+    detect-libc "^1.0.3"
+    xxhash-wasm "^0.4.2"
+
+"@parcel/logger@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.8.3.tgz#e14e4debafb3ca9e87c07c06780f9afc38b2712c"
+  integrity sha512-Kpxd3O/Vs7nYJIzkdmB6Bvp3l/85ydIxaZaPfGSGTYOfaffSOTkhcW9l6WemsxUrlts4za6CaEWcc4DOvaMOPA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/events" "2.8.3"
+
+"@parcel/markdown-ansi@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.8.3.tgz#1337d421bb1133ad178f386a8e1b746631bba4a1"
+  integrity sha512-4v+pjyoh9f5zuU/gJlNvNFGEAb6J90sOBwpKJYJhdWXLZMNFCVzSigxrYO+vCsi8G4rl6/B2c0LcwIMjGPHmFQ==
+  dependencies:
+    chalk "^4.1.0"
+
+"@parcel/namer-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.8.3.tgz#5304bee74beb4b9c1880781bdbe35be0656372f4"
+  integrity sha512-tJ7JehZviS5QwnxbARd8Uh63rkikZdZs1QOyivUhEvhN+DddSAVEdQLHGPzkl3YRk0tjFhbqo+Jci7TpezuAMw==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/node-resolver-core@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-2.8.3.tgz#581df074a27646400b3fed9da95297b616a7db8f"
+  integrity sha512-12YryWcA5Iw2WNoEVr/t2HDjYR1iEzbjEcxfh1vaVDdZ020PiGw67g5hyIE/tsnG7SRJ0xdRx1fQ2hDgED+0Ww==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    semver "^5.7.1"
+
+"@parcel/optimizer-css@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-css/-/optimizer-css-2.8.3.tgz#420a333f4b78f7ff15e69217dfed34421b1143ee"
+  integrity sha512-JotGAWo8JhuXsQDK0UkzeQB0UR5hDAKvAviXrjqB4KM9wZNLhLleeEAW4Hk8R9smCeQFP6Xg/N/NkLDpqMwT3g==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    browserslist "^4.6.6"
+    lightningcss "^1.16.1"
+    nullthrows "^1.1.1"
+
+"@parcel/optimizer-htmlnano@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.8.3.tgz#a71ab6f0f24160ef9f573266064438eff65e96d0"
+  integrity sha512-L8/fHbEy8Id2a2E0fwR5eKGlv9VYDjrH9PwdJE9Za9v1O/vEsfl/0T/79/x129l5O0yB6EFQkFa20MiK3b+vOg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    htmlnano "^2.0.0"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    svgo "^2.4.0"
+
+"@parcel/optimizer-image@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-image/-/optimizer-image-2.8.3.tgz#ea49b4245b4f7d60b38c7585c6311fb21d341baa"
+  integrity sha512-SD71sSH27SkCDNUNx9A3jizqB/WIJr3dsfp+JZGZC42tpD/Siim6Rqy9M4To/BpMMQIIiEXa5ofwS+DgTEiEHQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    detect-libc "^1.0.3"
+
+"@parcel/optimizer-svgo@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-svgo/-/optimizer-svgo-2.8.3.tgz#04da4efec6b623679539a84961bff6998034ba8a"
+  integrity sha512-9KQed99NZnQw3/W4qBYVQ7212rzA9EqrQG019TIWJzkA9tjGBMIm2c/nXpK1tc3hQ3e7KkXkFCQ3C+ibVUnHNA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    svgo "^2.4.0"
+
+"@parcel/optimizer-terser@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-terser/-/optimizer-terser-2.8.3.tgz#3a06d98d09386a1a0ae1be85376a8739bfba9618"
+  integrity sha512-9EeQlN6zIeUWwzrzu6Q2pQSaYsYGah8MtiQ/hog9KEPlYTP60hBv/+utDyYEHSQhL7y5ym08tPX5GzBvwAD/dA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    terser "^5.2.0"
+
+"@parcel/package-manager@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.8.3.tgz#ddd0d62feae3cf0fb6cc0537791b3a16296ad458"
+  integrity sha512-tIpY5pD2lH53p9hpi++GsODy6V3khSTX4pLEGuMpeSYbHthnOViobqIlFLsjni+QA1pfc8NNNIQwSNdGjYflVA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    semver "^5.7.1"
+
+"@parcel/packager-css@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.8.3.tgz#0eff34268cb4f5dfb53c1bbca85f5567aeb1835a"
+  integrity sha512-WyvkMmsurlHG8d8oUVm7S+D+cC/T3qGeqogb7sTI52gB6uiywU7lRCizLNqGFyFGIxcVTVHWnSHqItBcLN76lA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/packager-html@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.8.3.tgz#f9263b891aa4dd46c6e2fa2b07025a482132fff1"
+  integrity sha512-OhPu1Hx1RRKJodpiu86ZqL8el2Aa4uhBHF6RAL1Pcrh2EhRRlPf70Sk0tC22zUpYL7es+iNKZ/n0Rl+OWSHWEw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+
+"@parcel/packager-js@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.8.3.tgz#3ed11565915d73d12192b6901c75a6b820e4a83a"
+  integrity sha512-0pGKC3Ax5vFuxuZCRB+nBucRfFRz4ioie19BbDxYnvBxrd4M3FIu45njf6zbBYsI9eXqaDnL1b3DcZJfYqtIzw==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    globals "^13.2.0"
+    nullthrows "^1.1.1"
+
+"@parcel/packager-raw@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.8.3.tgz#bdec826df991e186cb58691cc45d12ad5c06676e"
+  integrity sha512-BA6enNQo1RCnco9MhkxGrjOk59O71IZ9DPKu3lCtqqYEVd823tXff2clDKHK25i6cChmeHu6oB1Rb73hlPqhUA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/packager-svg@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-svg/-/packager-svg-2.8.3.tgz#7233315296001c531cb55ca96b5f2ef672343630"
+  integrity sha512-mvIoHpmv5yzl36OjrklTDFShLUfPFTwrmp1eIwiszGdEBuQaX7JVI3Oo2jbVQgcN4W7J6SENzGQ3Q5hPTW3pMw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    posthtml "^0.16.4"
+
+"@parcel/plugin@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.8.3.tgz#7bb30a5775eaa6473c27f002a0a3ee7308d6d669"
+  integrity sha512-jZ6mnsS4D9X9GaNnvrixDQwlUQJCohDX2hGyM0U0bY2NWU8Km97SjtoCpWjq+XBCx/gpC4g58+fk9VQeZq2vlw==
+  dependencies:
+    "@parcel/types" "2.8.3"
+
+"@parcel/reporter-cli@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.8.3.tgz#12a4743b51b8fe6837f53c20e01bbf1f7336e8e4"
+  integrity sha512-3sJkS6tFFzgIOz3u3IpD/RsmRxvOKKiQHOTkiiqRt1l44mMDGKS7zANRnJYsQzdCsgwc9SOP30XFgJwtoVlMbw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    chalk "^4.1.0"
+    term-size "^2.2.1"
+
+"@parcel/reporter-dev-server@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.8.3.tgz#a0daa5cc015642684cea561f4e0e7116bbffdc1c"
+  integrity sha512-Y8C8hzgzTd13IoWTj+COYXEyCkXfmVJs3//GDBsH22pbtSFMuzAZd+8J9qsCo0EWpiDow7V9f1LischvEh3FbQ==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+
+"@parcel/resolver-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.8.3.tgz#5ae41e537ae4a793c1abb47f094482b9e2ac3535"
+  integrity sha512-k0B5M/PJ+3rFbNj4xZSBr6d6HVIe6DH/P3dClLcgBYSXAvElNDfXgtIimbjCyItFkW9/BfcgOVKEEIZOeySH/A==
+  dependencies:
+    "@parcel/node-resolver-core" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/runtime-browser-hmr@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.8.3.tgz#1fa74e1fbd1030b0a920c58afa3a9eb7dc4bcd1e"
+  integrity sha512-2O1PYi2j/Q0lTyGNV3JdBYwg4rKo6TEVFlYGdd5wCYU9ZIN9RRuoCnWWH2qCPj3pjIVtBeppYxzfVjPEHINWVg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+
+"@parcel/runtime-js@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.8.3.tgz#0baa4c8fbf77eabce05d01ccc186614968ffc0cd"
+  integrity sha512-IRja0vNKwvMtPgIqkBQh0QtRn0XcxNC8HU1jrgWGRckzu10qJWO+5ULgtOeR4pv9krffmMPqywGXw6l/gvJKYQ==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/runtime-react-refresh@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.8.3.tgz#381a942fb81e8f5ac6c7e0ee1b91dbf34763c3f8"
+  integrity sha512-2v/qFKp00MfG0234OdOgQNAo6TLENpFYZMbVbAsPMY9ITiqG73MrEsrGXVoGbYiGTMB/Toer/lSWlJxtacOCuA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    react-error-overlay "6.0.9"
+    react-refresh "^0.9.0"
+
+"@parcel/runtime-service-worker@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-service-worker/-/runtime-service-worker-2.8.3.tgz#54d92da9ff1dfbd27db0e84164a22fa59e99b348"
+  integrity sha512-/Skkw+EeRiwzOJso5fQtK8c9b452uWLNhQH1ISTodbmlcyB4YalAiSsyHCtMYD0c3/t5Sx4ZS7vxBAtQd0RvOw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/source-map@^2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@parcel/source-map/-/source-map-2.1.1.tgz#fb193b82dba6dd62cc7a76b326f57bb35000a782"
+  integrity sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==
+  dependencies:
+    detect-libc "^1.0.3"
+
+"@parcel/transformer-babel@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.8.3.tgz#286bc6cb9afe4c0259f0b28e0f2f47322a24b130"
+  integrity sha512-L6lExfpvvC7T/g3pxf3CIJRouQl+sgrSzuWQ0fD4PemUDHvHchSP4SNUVnd6gOytF3Y1KpnEZIunQGi5xVqQCQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    browserslist "^4.6.6"
+    json5 "^2.2.0"
+    nullthrows "^1.1.1"
+    semver "^5.7.0"
+
+"@parcel/transformer-css@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.8.3.tgz#d6c44100204e73841ad8e0f90472172ea8b9120c"
+  integrity sha512-xTqFwlSXtnaYen9ivAgz+xPW7yRl/u4QxtnDyDpz5dr8gSeOpQYRcjkd4RsYzKsWzZcGtB5EofEk8ayUbWKEUg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    browserslist "^4.6.6"
+    lightningcss "^1.16.1"
+    nullthrows "^1.1.1"
+
+"@parcel/transformer-html@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.8.3.tgz#5c68b28ee6b8c7a13b8aee87f7957ad3227bd83f"
+  integrity sha512-kIZO3qsMYTbSnSpl9cnZog+SwL517ffWH54JeB410OSAYF1ouf4n5v9qBnALZbuCCmPwJRGs4jUtE452hxwN4g==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.10.1"
+    posthtml-render "^3.0.0"
+    semver "^5.7.1"
+    srcset "4"
+
+"@parcel/transformer-image@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.8.3.tgz#73805b2bfc3c8919d7737544e5f8be39e3f303fe"
+  integrity sha512-cO4uptcCGTi5H6bvTrAWEFUsTNhA4kCo8BSvRSCHA2sf/4C5tGQPHt3JhdO0GQLPwZRCh/R41EkJs5HZ8A8DAg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/transformer-js@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.8.3.tgz#fe400df428394d1e7fe5afb6dea5c7c858e44f03"
+  integrity sha512-9Qd6bib+sWRcpovvzvxwy/PdFrLUXGfmSW9XcVVG8pvgXsZPFaNjnNT8stzGQj1pQiougCoxMY4aTM5p1lGHEQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    "@swc/helpers" "^0.4.12"
+    browserslist "^4.6.6"
+    detect-libc "^1.0.3"
+    nullthrows "^1.1.1"
+    regenerator-runtime "^0.13.7"
+    semver "^5.7.1"
+
+"@parcel/transformer-json@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.8.3.tgz#25deb3a5138cc70a83269fc5d39d564609354d36"
+  integrity sha512-B7LmVq5Q7bZO4ERb6NHtRuUKWGysEeaj9H4zelnyBv+wLgpo4f5FCxSE1/rTNmP9u1qHvQ3scGdK6EdSSokGPg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    json5 "^2.2.0"
+
+"@parcel/transformer-postcss@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.8.3.tgz#df4fdc1c90893823445f2a8eb8e2bdd0349ccc58"
+  integrity sha512-e8luB/poIlz6jBsD1Izms+6ElbyzuoFVa4lFVLZnTAChI3UxPdt9p/uTsIO46HyBps/Bk8ocvt3J4YF84jzmvg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    clone "^2.1.1"
+    nullthrows "^1.1.1"
+    postcss-value-parser "^4.2.0"
+    semver "^5.7.1"
+
+"@parcel/transformer-posthtml@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.8.3.tgz#7c3912a5a631cb26485f6464e0d6eeabb6f1e718"
+  integrity sha512-pkzf9Smyeaw4uaRLsT41RGrPLT5Aip8ZPcntawAfIo+KivBQUV0erY1IvHYjyfFzq1ld/Fo2Ith9He6mxpPifA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.10.1"
+    posthtml-render "^3.0.0"
+    semver "^5.7.1"
+
+"@parcel/transformer-raw@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.8.3.tgz#3a22213fe18a5f83fd78889cb49f06e059cfead7"
+  integrity sha512-G+5cXnd2/1O3nV/pgRxVKZY/HcGSseuhAe71gQdSQftb8uJEURyUHoQ9Eh0JUD3MgWh9V+nIKoyFEZdf9T0sUQ==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/transformer-react-refresh-wrap@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.8.3.tgz#8b0392638405dd470a886002229f7889d5464822"
+  integrity sha512-q8AAoEvBnCf/nPvgOwFwKZfEl/thwq7c2duxXkhl+tTLDRN2vGmyz4355IxCkavSX+pLWSQ5MexklSEeMkgthg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    react-refresh "^0.9.0"
+
+"@parcel/transformer-svg@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.8.3.tgz#4df959cba4ebf45d7aaddd540f752e6e84df38b2"
+  integrity sha512-3Zr/gBzxi1ZH1fftH/+KsZU7w5GqkmxlB0ZM8ovS5E/Pl1lq1t0xvGJue9m2VuQqP8Mxfpl5qLFmsKlhaZdMIQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.10.1"
+    posthtml-render "^3.0.0"
+    semver "^5.7.1"
+
+"@parcel/types@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.8.3.tgz#3306bc5391b6913bd619914894b8cd84a24b30fa"
+  integrity sha512-FECA1FB7+0UpITKU0D6TgGBpGxYpVSMNEENZbSJxFSajNy3wrko+zwBKQmFOLOiPcEtnGikxNs+jkFWbPlUAtw==
+  dependencies:
+    "@parcel/cache" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/package-manager" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/workers" "2.8.3"
+    utility-types "^3.10.0"
+
+"@parcel/utils@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.8.3.tgz#0d56c9e8e22c119590a5e044a0e01031965da40e"
+  integrity sha512-IhVrmNiJ+LOKHcCivG5dnuLGjhPYxQ/IzbnF2DKNQXWBTsYlHkJZpmz7THoeLtLliGmSOZ3ZCsbR8/tJJKmxjA==
+  dependencies:
+    "@parcel/codeframe" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/markdown-ansi" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    chalk "^4.1.0"
+
+"@parcel/watcher@^2.0.7":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.1.0.tgz#5f32969362db4893922c526a842d8af7a8538545"
+  integrity sha512-8s8yYjd19pDSsBpbkOHnT6Z2+UJSuLQx61pCFM0s5wSRvKCEMDjd/cHY3/GI1szHIWbpXpsJdg3V6ISGGx9xDw==
+  dependencies:
+    is-glob "^4.0.3"
+    micromatch "^4.0.5"
+    node-addon-api "^3.2.1"
+    node-gyp-build "^4.3.0"
+
+"@parcel/workers@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.8.3.tgz#255450ccf4db234082407e4ddda5fd575f08c235"
+  integrity sha512-+AxBnKgjqVpUHBcHLWIHcjYgKIvHIpZjN33mG5LG9XXvrZiqdWvouEzqEXlVLq5VzzVbKIQQcmsvRy138YErkg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    chrome-trace-event "^1.0.2"
+    nullthrows "^1.1.1"
+
+"@swc/helpers@^0.4.12":
+  version "0.4.14"
+  resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74"
+  integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
+  dependencies:
+    tslib "^2.4.0"
+
+"@trysound/sax@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
+  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+abortcontroller-polyfill@^1.1.9:
+  version "1.7.5"
+  resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed"
+  integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==
+
+acorn@^8.5.0:
+  version "8.8.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
+  integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
+
+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.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+base-x@^3.0.8:
+  version "3.0.9"
+  resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
+  integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
+braces@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browserslist@^4.6.6:
+  version "4.21.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
+  integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==
+  dependencies:
+    caniuse-lite "^1.0.30001400"
+    electron-to-chromium "^1.4.251"
+    node-releases "^2.0.6"
+    update-browserslist-db "^1.0.9"
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+caniuse-lite@^1.0.30001400:
+  version "1.0.30001446"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001446.tgz#6d4ba828ab19f49f9bcd14a8430d30feebf1e0c5"
+  integrity sha512-fEoga4PrImGcwUUGEol/PoFCSBnSkA9drgdkxXkJLsUBOnJ8rs3zDv6ApqYXGQFOyMPsjh79naWhF4DAxbF8rw==
+
+chalk@^2.0.0:
+  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@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chrome-trace-event@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+  integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+clone@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
+
+color-convert@^1.9.0:
+  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 sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+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==
+
+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==
+
+commander@^7.0.0, commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+cosmiconfig@^7.0.1:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
+  integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.2.1"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.10.0"
+
+css-select@^4.1.3:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
+  integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^6.0.1"
+    domhandler "^4.3.1"
+    domutils "^2.8.0"
+    nth-check "^2.0.1"
+
+css-tree@^1.1.2, css-tree@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+  dependencies:
+    mdn-data "2.0.14"
+    source-map "^0.6.1"
+
+css-what@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
+  integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
+
+csso@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+  dependencies:
+    css-tree "^1.1.2"
+
+detect-libc@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+  integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
+
+dom-serializer@^1.0.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
+  integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.0"
+    entities "^2.0.0"
+
+domelementtype@^2.0.1, domelementtype@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
+domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+  integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.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@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c"
+  integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==
+
+electron-to-chromium@^1.4.251:
+  version "1.4.284"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
+  integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
+
+entities@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
+entities@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
+  integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
+
+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"
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+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 sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+get-port@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119"
+  integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==
+
+globals@^13.2.0:
+  version "13.19.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8"
+  integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==
+  dependencies:
+    type-fest "^0.20.2"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+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==
+
+htmlnano@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.3.tgz#50ee639ed63357d4a6c01309f52a35892e4edc2e"
+  integrity sha512-S4PGGj9RbdgW8LhbILNK7W9JhmYP8zmDY7KDV/8eCiJBQJlbmltp5I0gv8c5ntLljfdxxfmJ+UJVSqyH4mb41A==
+  dependencies:
+    cosmiconfig "^7.0.1"
+    posthtml "^0.16.5"
+    timsort "^0.3.0"
+
+htmlparser2@^7.1.1:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5"
+  integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.2"
+    domutils "^2.8.0"
+    entities "^3.0.1"
+
+import-fresh@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.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 sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-json@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff"
+  integrity sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+"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==
+
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json5@^2.2.0, json5@^2.2.1:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+  integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+lightningcss-darwin-arm64@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.18.0.tgz#bcd7d494d99c69947abd71136a42e80dfa80c682"
+  integrity sha512-OqjydwtiNPgdH1ByIjA1YzqvDG/OMR6L3LPN6wRl1729LB0y4Mik7L06kmZaTb+pvUHr+NmDd2KCwnlrQ4zO3w==
+
+lightningcss-darwin-x64@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.18.0.tgz#952abea2405fe2bb8dd0bb57a9d5590f8d1d6414"
+  integrity sha512-mNiuPHj89/JHZmJMp+5H8EZSt6EL5DZRWJ31O6k3DrLLnRIQjXuXdDdN8kP7LoIkeWI5xvyD60CsReJm+YWYAw==
+
+lightningcss-linux-arm-gnueabihf@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.18.0.tgz#23ca85e05dc4def9b4975aef307554ef292b56cd"
+  integrity sha512-S+25JjI6601HiAVoTDXW6SqH+E94a+FHA7WQqseyNHunOgVWKcAkNEc2LJvVxgwTq6z41sDIb9/M3Z9wa9lk4A==
+
+lightningcss-linux-arm64-gnu@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.18.0.tgz#6c8e0a6e2c8b44cf180f3a0f0740402e8f656155"
+  integrity sha512-JSqh4+21dCgBecIQUet35dtE4PhhSEMyqe3y0ZNQrAJQ5kyUPSQHiw81WXnPJcOSTTpG0TyMLiC8K//+BsFGQA==
+
+lightningcss-linux-arm64-musl@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.18.0.tgz#88393c101cf236ea0cdc97fddd66b82db964d835"
+  integrity sha512-2FWHa8iUhShnZnqhn2wfIcK5adJat9hAAaX7etNsoXJymlliDIOFuBQEsba2KBAZSM4QqfQtvRdR7m8i0I7ybQ==
+
+lightningcss-linux-x64-gnu@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.18.0.tgz#ad068d24836568337bfe545650565e13f813c8ee"
+  integrity sha512-plCPGQJtDZHcLVKVRLnQVF2XRsIC32WvuJhQ7fJ7F6BV98b/VZX0OlX05qUaOESD9dCDHjYSfxsgcvOKgCWh7A==
+
+lightningcss-linux-x64-musl@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.18.0.tgz#4d84de26b8185aa42450e0f4c83bbfb5a36ae750"
+  integrity sha512-na+BGtVU6fpZvOHKhnlA0XHeibkT3/46nj6vLluG3kzdJYoBKU6dIl7DSOk++8jv4ybZyFJ0aOFMMSc8g2h58A==
+
+lightningcss-win32-x64-msvc@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.18.0.tgz#f83952d16b83dfce65f4615f87c867769220d117"
+  integrity sha512-5qeAH4RMNy2yMNEl7e5TI6upt/7xD2ZpHWH4RkT8iJ7/6POS5mjHbXWUO9Q1hhDhqkdzGa76uAdMzEouIeCyNw==
+
+lightningcss@^1.16.1:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.18.0.tgz#ca3327a1a7571a83bbb9733ed4e4cded775bdadf"
+  integrity sha512-uk10tNxi5fhZqU93vtYiQgx/8a9f0Kvtj5AXIm+VlOXY+t/DWDmCZWJEkZJmmALgvbS6aAW8or+Kq85eJ6TDTw==
+  dependencies:
+    detect-libc "^1.0.3"
+  optionalDependencies:
+    lightningcss-darwin-arm64 "1.18.0"
+    lightningcss-darwin-x64 "1.18.0"
+    lightningcss-linux-arm-gnueabihf "1.18.0"
+    lightningcss-linux-arm64-gnu "1.18.0"
+    lightningcss-linux-arm64-musl "1.18.0"
+    lightningcss-linux-x64-gnu "1.18.0"
+    lightningcss-linux-x64-musl "1.18.0"
+    lightningcss-win32-x64-msvc "1.18.0"
+
+lines-and-columns@^1.1.6:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+lmdb@2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.5.2.tgz#37e28a9fb43405f4dc48c44cec0e13a14c4a6ff1"
+  integrity sha512-V5V5Xa2Hp9i2XsbDALkBTeHXnBXh/lEmk9p22zdr7jtuOIY9TGhjK6vAvTpOOx9IKU4hJkRWZxn/HsvR1ELLtA==
+  dependencies:
+    msgpackr "^1.5.4"
+    node-addon-api "^4.3.0"
+    node-gyp-build-optional-packages "5.0.3"
+    ordered-binary "^1.2.4"
+    weak-lru-cache "^1.2.2"
+  optionalDependencies:
+    "@lmdb/lmdb-darwin-arm64" "2.5.2"
+    "@lmdb/lmdb-darwin-x64" "2.5.2"
+    "@lmdb/lmdb-linux-arm" "2.5.2"
+    "@lmdb/lmdb-linux-arm64" "2.5.2"
+    "@lmdb/lmdb-linux-x64" "2.5.2"
+    "@lmdb/lmdb-win32-x64" "2.5.2"
+
+loose-envify@^1.1.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"
+
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
+micromatch@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
+msgpackr-extract@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.2.0.tgz#4bb749b58d9764cfdc0d91c7977a007b08e8f262"
+  integrity sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog==
+  dependencies:
+    node-gyp-build-optional-packages "5.0.3"
+  optionalDependencies:
+    "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-linux-arm" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-linux-x64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-win32-x64" "2.2.0"
+
+msgpackr@^1.5.4:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.1.tgz#2298aed8a14f83e99df77d344cbda3e436f29b5b"
+  integrity sha512-05fT4J8ZqjYlR4QcRDIhLCYKUOHXk7C/xa62GzMKj74l3up9k2QZ3LgFc6qWdsPHl91QA2WLWqWc8b8t7GLNNw==
+  optionalDependencies:
+    msgpackr-extract "^2.2.0"
+
+node-addon-api@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
+  integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
+
+node-addon-api@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
+  integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
+
+node-gyp-build-optional-packages@5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17"
+  integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==
+
+node-gyp-build@^4.3.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055"
+  integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==
+
+node-releases@^2.0.6:
+  version "2.0.8"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
+  integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==
+
+nth-check@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
+  integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
+  dependencies:
+    boolbase "^1.0.0"
+
+nullthrows@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
+  integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
+
+ordered-binary@^1.2.4:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ordered-binary/-/ordered-binary-1.4.0.tgz#6bb53d44925f3b8afc33d1eed0fa15693b211389"
+  integrity sha512-EHQ/jk4/a9hLupIKxTfUsQRej1Yd/0QLQs3vGvIqg5ZtCYSzNhkzHoZc7Zf4e4kUlDaC3Uw8Q/1opOLNN2OKRQ==
+
+parcel@^2.8.3:
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.8.3.tgz#1ff71d7317274fd367379bc7310a52c6b75d30c2"
+  integrity sha512-5rMBpbNE72g6jZvkdR5gS2nyhwIXaJy8i65osOqs/+5b7zgf3eMKgjSsDrv6bhz3gzifsba6MBJiZdBckl+vnA==
+  dependencies:
+    "@parcel/config-default" "2.8.3"
+    "@parcel/core" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/events" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/package-manager" "2.8.3"
+    "@parcel/reporter-cli" "2.8.3"
+    "@parcel/reporter-dev-server" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    chalk "^4.1.0"
+    commander "^7.0.0"
+    get-port "^4.2.0"
+    v8-compile-cache "^2.0.0"
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+posthtml-parser@^0.10.1:
+  version "0.10.2"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.10.2.tgz#df364d7b179f2a6bf0466b56be7b98fd4e97c573"
+  integrity sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==
+  dependencies:
+    htmlparser2 "^7.1.1"
+
+posthtml-parser@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.11.0.tgz#25d1c7bf811ea83559bc4c21c189a29747a24b7a"
+  integrity sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==
+  dependencies:
+    htmlparser2 "^7.1.1"
+
+posthtml-render@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-3.0.0.tgz#97be44931496f495b4f07b99e903cc70ad6a3205"
+  integrity sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==
+  dependencies:
+    is-json "^2.0.1"
+
+posthtml@^0.16.4, posthtml@^0.16.5:
+  version "0.16.6"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.16.6.tgz#e2fc407f67a64d2fa3567afe770409ffdadafe59"
+  integrity sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==
+  dependencies:
+    posthtml-parser "^0.11.0"
+    posthtml-render "^3.0.0"
+
+process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+react-dom@^18.2.0:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
+  integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
+  dependencies:
+    loose-envify "^1.1.0"
+    scheduler "^0.23.0"
+
+react-error-overlay@6.0.9:
+  version "6.0.9"
+  resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
+  integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
+
+react-refresh@^0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
+  integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
+
+react@^18.2.0:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
+  integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
+  dependencies:
+    loose-envify "^1.1.0"
+
+regenerator-runtime@^0.13.7:
+  version "0.13.11"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+safe-buffer@^5.0.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+scheduler@^0.23.0:
+  version "0.23.0"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
+  integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
+  dependencies:
+    loose-envify "^1.1.0"
+
+semver@^5.7.0, semver@^5.7.1:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+source-map-support@~0.5.20:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, 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==
+
+srcset@4:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4"
+  integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+supports-color@^5.3.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@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+svgo@^2.4.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
+  integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
+  dependencies:
+    "@trysound/sax" "0.2.0"
+    commander "^7.2.0"
+    css-select "^4.1.3"
+    css-tree "^1.1.3"
+    csso "^4.2.0"
+    picocolors "^1.0.0"
+    stable "^0.1.8"
+
+term-size@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
+  integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
+
+terser@^5.2.0:
+  version "5.16.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880"
+  integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.2"
+    acorn "^8.5.0"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+tslib@^2.4.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
+  integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
+
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+update-browserslist-db@^1.0.9:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
+  integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
+utility-types@^3.10.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
+  integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
+
+v8-compile-cache@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
+
+weak-lru-cache@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"
+  integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==
+
+xxhash-wasm@^0.4.2:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79"
+  integrity sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==
+
+yaml@^1.10.0:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
diff --git a/users/wpcarro/terraform/.gitignore b/users/wpcarro/terraform/.gitignore
new file mode 100644
index 000000000000..f437e99d802a
--- /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 000000000000..b8625c3b8d9b
--- /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 000000000000..7ebc82d6451d
--- /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 000000000000..bb893387ecd3
--- /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 000000000000..58a52392cae0
--- /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 000000000000..6835c73bd87c
--- /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 000000000000..7cddcef478d8
--- /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 000000000000..058ff6b274af
--- /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 000000000000..6560926eaefd
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/.envrc
@@ -0,0 +1,9 @@
+source_up
+
+# 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 000000000000..e92078303bec
--- /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/README.md b/users/wpcarro/tools/monzo_ynab/README.md
new file mode 100644
index 000000000000..c0c0c772f646
--- /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 000000000000..b66bacb10687
--- /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/main.go b/users/wpcarro/tools/monzo_ynab/main.go
new file mode 100644
index 000000000000..900deac0cbdb
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/main.go
@@ -0,0 +1,48 @@
+// 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 (
+	"monzoClient"
+	"monzoSerde"
+	"os"
+	"ynabClient"
+	"ynabSerde"
+)
+
+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() {
+	monzo := monzoClient.Create()
+	txs := monzo.TransactionsLast24Hours()
+	var ynabTxs []ynabSerde.Transaction
+	for _, tx := range txs {
+		ynabTxs = append(ynabTxs, toYnab(tx))
+	}
+	ynabClient.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 000000000000..9621ffc5ad51
--- /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) TransactionsLast24Hours() []monzoSerde.Transaction {
+	token := tokens.GetState().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 000000000000..e2f55dad4597
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/monzo/serde.go
@@ -0,0 +1,72 @@
+// This package hosts the serialization and deserialization logic for all of the
+// data types with which our application interacts from the Monzo API.
+package monzoSerde
+
+import (
+	"encoding/json"
+	"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
+}
diff --git a/users/wpcarro/tools/monzo_ynab/requests.txt b/users/wpcarro/tools/monzo_ynab/requests.txt
new file mode 100644
index 000000000000..2da17c0b326a
--- /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/tokens.go b/users/wpcarro/tools/monzo_ynab/tokens.go
new file mode 100644
index 000000000000..01b57d3daafd
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/tokens.go
@@ -0,0 +1,279 @@
+// 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 tokens
+
+////////////////////////////////////////////////////////////////////////////////
+// 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
+}
+
+func StartServer() {
+	// 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/ynab/client.go b/users/wpcarro/tools/monzo_ynab/ynab/client.go
new file mode 100644
index 000000000000..e63010b28189
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/ynab/client.go
@@ -0,0 +1,9 @@
+package ynabClient
+
+import (
+	"ynabSerde"
+)
+
+// See requests.txt for more details.
+func PostTransactions(txs []ynabSerde.Transaction) {
+}
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 000000000000..45dd921b2403
--- /dev/null
+++ b/users/wpcarro/tools/monzo_ynab/ynab/serde.go
@@ -0,0 +1,44 @@
+// This package hosts the serialization and deserialization logic for all of the
+// data types with which our application interacts from the YNAB API.
+package ynabSerde
+
+import (
+	"encoding/json"
+	"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
+}
diff --git a/users/wpcarro/tools/rfcToKindle/LICENSE b/users/wpcarro/tools/rfcToKindle/LICENSE
new file mode 100644
index 000000000000..7a4a3ea2424c
--- /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 000000000000..e7b4fa841ef6
--- /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 000000000000..ca87abdee012
--- /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 000000000000..0f4f2dd9ec4f
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..fd24495035ba
--- /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 000000000000..807a75c28450
--- /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 000000000000..04906ece91f7
--- /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 000000000000..f777c13fefae
--- /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 000000000000..bbdcdc5f1a85
--- /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 000000000000..ae3fd76a46a3
--- /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 000000000000..e8298ea65401
--- /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 000000000000..7d022828ee97
--- /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 000000000000..d99c7cb863ce
--- /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/systemd-shell/default.nix b/users/wpcarro/tools/systemd-shell/default.nix
new file mode 100644
index 000000000000..eace76b7087b
--- /dev/null
+++ b/users/wpcarro/tools/systemd-shell/default.nix
@@ -0,0 +1,8 @@
+{ pkgs, ... }:
+
+pkgs.python310Packages.buildPythonApplication {
+  pname = "systemd-shell";
+  version = "0.0.1";
+  src = ./.;
+  doCheck = false;
+}
diff --git a/users/wpcarro/tools/systemd-shell/setup.py b/users/wpcarro/tools/systemd-shell/setup.py
new file mode 100644
index 000000000000..f45e058e672e
--- /dev/null
+++ b/users/wpcarro/tools/systemd-shell/setup.py
@@ -0,0 +1,9 @@
+from setuptools import setup
+
+setup(
+    name="systemd-shell",
+    version="0.0.1",
+    author="William Carroll",
+    author_email="wpcarro@gmail.com",
+    scripts=["systemd-shell"],
+)
diff --git a/users/wpcarro/tools/systemd-shell/systemd-shell b/users/wpcarro/tools/systemd-shell/systemd-shell
new file mode 100644
index 000000000000..646d59143afd
--- /dev/null
+++ b/users/wpcarro/tools/systemd-shell/systemd-shell
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+# Drop into a new shell environment with the same variables defined in a systemd
+# unit file (for debugging purposes).
+#
+# USAGE:
+#   $ systemd-shell -u buildkite-agent-foundation-1.service
+
+import argparse
+import os
+
+def parse_env(entry):
+    x = entry[12:].split("=", 1)
+    return x[0].removeprefix("\"").removesuffix("\""), x[1].removeprefix("\"").removesuffix("\"")
+
+def run(unit):
+  envfile = []
+  print("--- Environment ---")
+  for line in open(f"/etc/systemd/system/{unit}").readlines():
+      if line.startswith("Environment="):
+          key, val = parse_env(line[:-1])
+          print(f"export {key}={val}")
+          envfile.append(f"{key}={val}")
+      else:
+          continue
+  print()
+
+  env = " ".join(envfile)
+  print("--- Command ---")
+  os.system(f"/usr/bin/env {env} /bin/sh")
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-u", "--unit", type=str, required=True)
+    args = parser.parse_args()
+    run(args.unit)
diff --git a/users/wpcarro/tools/url-blocker/.envrc b/users/wpcarro/tools/url-blocker/.envrc
new file mode 100644
index 000000000000..a4a62da526d3
--- /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 000000000000..926412ce91f9
--- /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 000000000000..a6063f32562d
--- /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 000000000000..ae24aa41b7ca
--- /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 000000000000..95e4dc9a90c1
--- /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 000000000000..aa5c906edc90
--- /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/tools/wpcarro-deps.nix b/users/wpcarro/tools/wpcarro-deps.nix
new file mode 100644
index 000000000000..5eafd0bfda14
--- /dev/null
+++ b/users/wpcarro/tools/wpcarro-deps.nix
@@ -0,0 +1,8 @@
+# Shell derivation to invoke //nix/lazy-deps with the dependencies that should
+# be lazily made available in wpcarro's users dir in depot.
+{ pkgs, depot, ... }:
+
+depot.nix.lazy-deps {
+  import-gpg.attr = "users.wpcarro.configs.import-gpg";
+  export-gpg.attr = "users.wpcarro.configs.export-gpg";
+}
diff --git a/users/wpcarro/utils/README.md b/users/wpcarro/utils/README.md
new file mode 100644
index 000000000000..0724dff2a992
--- /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 000000000000..2bc061d3661b
--- /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 000000000000..46d30acfa24e
--- /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 000000000000..d7d5e34e991b
--- /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 000000000000..30420571c034
--- /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 000000000000..3a9dbd4d2b60
--- /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 000000000000..27541b0f39b4
--- /dev/null
+++ b/users/wpcarro/website/blog/default.nix
@@ -0,0 +1,47 @@
+{ 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";
+    staticUrl = "https://static.tvl.fyi/latest";
+    footer = "";
+  };
+
+  posts = sort (x: y: x.date > y.date)
+    (filter includePost (list post (import ./posts.nix)));
+
+  rendered = pkgs.runCommand "blog-posts" { } ''
+    mkdir -p $out
+
+    ${lib.concatStringsSep "\n" (map (post:
+      "cp ${renderPost config post} $out/${post.key}.html"
+    ) posts)}
+  '';
+
+  formatDate = date: readFile (pkgs.runCommand "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.runCommand "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 000000000000..e69de29bb2d1
--- /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 000000000000..2741292aa930
--- /dev/null
+++ b/users/wpcarro/website/blog/fragments/post.html
@@ -0,0 +1,8 @@
+<li class="pb-6 md:pb-10 text-md md:text-xl">
+  <h2 class="text-bold">
+    <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 000000000000..a85a4b71101e
--- /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 000000000000..31fb0c83d8f0
--- /dev/null
+++ b/users/wpcarro/website/blog/posts.nix
@@ -0,0 +1,116 @@
+# 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;
+  }
+  {
+    key = "csharp-unused-variables";
+    title = "Unused Variables Broke Prod";
+    date = 1655840877;
+    content = ./posts/csharp-unused-variables.md;
+    draft = false;
+  }
+  {
+    key = "restic-field-guide";
+    title = "Beginner's Field Guide to restic";
+    date = 1656645093;
+    content = ./posts/restic.md;
+    draft = false;
+  }
+  {
+    key = "tee-time";
+    title = "tee time";
+    date = 1657597870;
+    content = ./posts/tee-time.md;
+    draft = false;
+  }
+  {
+    key = "ssh-oddities";
+    title = "SSH Oddities";
+    date = 1657647994;
+    content = ./posts/ssh-oddities.md;
+    draft = false;
+  }
+  {
+    key = "nix-shell";
+    title = "nix-shell (note to self)";
+    date = 1664902186;
+    content = ./posts/nix-shell-note.md;
+    draft = false;
+  }
+  {
+    key = "git-filter-repo-note";
+    title = "git-filter-repo (note to self)";
+    date = 1665163559;
+    content = ./posts/git-filter-repo-note.md;
+    draft = false;
+  }
+  {
+    key = "nixos-disk-full-note";
+    title = "disk full (note to self)";
+    date = 1666801882;
+    content = ./posts/nixos-disk-full-note.md;
+    draft = false;
+  }
+  {
+    key = "git-rev-refs";
+    title = "git revision numbers as refs (note to self)";
+    date = 1666823030;
+    content = ./posts/git-rev-refs.md;
+    draft = false;
+  }
+  {
+    key = "import-subtree-checklist";
+    title = "Checklist for Importing Subtrees";
+    date = 1666903846;
+    content = ./posts/importing-subtrees.md;
+    draft = false;
+  }
+  {
+    key = "nix-env-note";
+    title = "nix-env (note to self)";
+    date = 1667343279;
+    content = ./posts/nix-env-note.md;
+    draft = false;
+  }
+  {
+    key = "nginx-virtual-host-note";
+    title = "Nginx Virtual Host (note to self)";
+    date = 1668448541;
+    content = ./posts/nginx-curl-note.md;
+    draft = false;
+  }
+  {
+    key = "tcp-tunneling-note";
+    title = "TCP Tunneling (note to self)";
+    date = 1668709613;
+    content = ./posts/tcp-tunneling-note.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 000000000000..24474e6dfe48
--- /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 000000000000..f781a60873c7
--- /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/csharp-unused-variables.md b/users/wpcarro/website/blog/posts/csharp-unused-variables.md
new file mode 100644
index 000000000000..a5b62647bcde
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/csharp-unused-variables.md
@@ -0,0 +1,38 @@
+**Problem**: This morning we broke production because (believe it or not) an
+unused variable went undetected.
+
+**Solution**: Consume the variable in the relevant codepath.
+
+**Further AI**: Treat unused variables as errors (which will block CI).
+
+## Warning/Disclaimer
+
+I am not a C# programmer. I know close to nothing about C#. But at `$WORK`, one
+of our codebases is written in C#, so occasionally I interface with it.
+
+## Treating Unused Variables as Errors
+
+C# uses `.csproj` files to configure projects. The following changes to our
+`.csproj` file WAI'd:
+
+```diff
++    <!-- IDE0059: Remove unnecessary value assignment -->
++    <WarningsAsErrors>IDE0059</WarningsAsErrors>
++    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
+```
+
+However, supporting this turned out to be a ~1h adventure... Why was this
+unexpectedly difficult? As it turns out, there are the 3x promising compiler
+warnings that I had to discover/try:
+
+- `CS0219`: doesn't WAI (see "Note" here: https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs0219)
+- `CA1804`: silently unsupported (replaced by `IDE0059`)
+- `IDE0059`: WAIs
+
+Legend:
+- `CS`: stands for C#
+- `CA`: stands for Code Analysis (I *think* a Visual Studio concept)
+- `IDE`: stands for IDE (I think *also* a Visual Studio concept)
+
+For `CA` and `IDE` prefixed warnings, `EnforceCodeStyleInBuild` must also be
+enabled.
diff --git a/users/wpcarro/website/blog/posts/git-filter-repo-note.md b/users/wpcarro/website/blog/posts/git-filter-repo-note.md
new file mode 100644
index 000000000000..e5fbb05f5cd2
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/git-filter-repo-note.md
@@ -0,0 +1,59 @@
+## Background
+
+- I recently used `git-filter-repo` to scrub cleartext secrets from a
+  repository.
+- We pin some services' deployments to commit SHAs.
+- These commit SHAs are no longer reachable from `origin/main`.
+
+## Problem
+
+If `git` garbage-collects any of the commits to which services are pinned, and
+that service attempts to redeploy, the deployment will fail.
+
+`git for-each-ref --contains $SHA` will report all of the refs that can reach
+some commit, `$SHA`. This may report things like:
+- `refs/replace` (i.e. `git-filter-repo` artifacts)
+- `refs/stash`
+- some local branches
+- some remote branches
+
+One solution might involve creating references to avoid garbage-collection. But
+if any of our pinned commits contains sensitive cleartext we *want* to ensure
+that `git` purges these.
+
+Instead let's find the SHAs of the new, rewritten commits and replace the pinned
+versions with those.
+
+## Solution
+
+Essentially we want to find a commit with the same *tree* state as the currently
+pinned commit. Here are two ways to get that info...
+
+This way is indirect, but provides more context about the change:
+
+```shell
+λ git cat-file -p $SHA
+tree d011a1dd4a3c5c4c6455ab3592fa2bf71d551d22 # <-- copy this tree info
+parent ba88bbf8de61be932184631244d2ec0ec8205cb8
+author William Carroll <wpcarro@gmail.com> 1664993052 -0700
+committer William Carroll <wpcarro@gmail.com> 1665116042 -0700
+
+feat(florp): Florp can now flarp
+
+You're welcome :)
+```
+
+This way is more direct (read: code-golf-friendly):
+
+```shell
+λ git log -1 --format=%T $SHA
+```
+
+Now that we have the SHA of the desired tree state, let's use it to query `git`
+for commits with the same tree SHA.
+
+```shell
+λ git log --format='%H %T' | grep $(git log --format=%T -1 $SHA) | awk '{ print $1 }'
+```
+
+Hopefully this helps!
diff --git a/users/wpcarro/website/blog/posts/git-rev-refs.md b/users/wpcarro/website/blog/posts/git-rev-refs.md
new file mode 100644
index 000000000000..fdc0aaf5cc9f
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/git-rev-refs.md
@@ -0,0 +1,85 @@
+## Credit
+
+Credit goes to `tazjin@` for this idea :)
+
+## Background
+
+Using `git` revisions to pin versions is nice, but git SHAs aren't very
+human-friendly:
+
+- They're difficult to type.
+- They're difficult to say in conversation.
+- They're difficult to compare. e.g. Which is newer? `2911fcd` or `db6ac90`?
+
+## Solution
+
+Let's assign monotonically increasing natural numbers to each of
+our repo's mainline commits and create `git` refs so we can use references like
+`r/123` instead of `2911fcd`.
+
+- They're easy to type: `r/123`
+- They're easy to say in conversion: "Check-out rev one-twenty-three."
+- They're easy to compare: `r/123` is an earlier version than `r/147`.
+
+## Backfilling
+
+Let's start-off by assigning "revision numbers" as refs for each of the mainline
+commits:
+
+```shell
+for commit in $(git rev-list --first-parent HEAD); do
+  git update-ref "refs/r/$(git rev-list --count --first-parent $commit)" $commit
+done
+```
+
+We can verify with:
+
+```shell
+λ git log --first-parent --oneline
+```
+
+If everything looks good, we can publish the refs to the remote:
+
+```shell
+λ git push origin 'refs/r/*:refs/r/*'
+```
+
+## Staying Current
+
+In order to make sure that any newly merged commits have an associated revision
+number as a ref, add something like the following to your CI system to run on
+the builds of your repo's mainline branch:
+
+```shell
+λ git push origin "HEAD:refs/r/$(git rev-list --count --first-parent HEAD)"
+```
+
+## Summary
+
+To verify that the remote now has the expected refs, we can use:
+
+```shell
+λ git ls-remote origin | less # grep for refs/r
+```
+
+If that looks good, you should now be able to *manually* fetch the refs with:
+
+```shell
+λ git fetch origin 'refs/r/*:refs/r/*'
+```
+
+Or you can use `git config` to automate this:
+
+```shell
+λ git config --add remote.origin.fetch '+refs/r/*:refs/r/*'
+λ git fetch origin
+```
+
+Now you can run fun commands like:
+
+```shell
+λ git show r/1234
+λ git diff r/123{4,8} # see changes from 1234 -> 1238
+```
+
+Thanks for reading!
diff --git a/users/wpcarro/website/blog/posts/importing-subtrees.md b/users/wpcarro/website/blog/posts/importing-subtrees.md
new file mode 100644
index 000000000000..e1070fc3b922
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/importing-subtrees.md
@@ -0,0 +1,147 @@
+## Background
+
+Sometimes you need to merge one Git repo into another. This is a common task
+when administrating a monorepo.
+
+Here's a checklist that I follow:
+
+1. Detect leaked secrets.
+1. Rotate leaked secrets.
+1. Purge leaked secrets from repo history.
+1. Create mainline references to branches (for deployments).
+1. Subtree-merge into the target repo.
+1. Format the code.
+1. Celebrate!
+
+## Secrets
+
+**Note:** If you notice any leaked secrets, first and foremost rotate them
+before moving on...
+
+`gitleaks` supports `gitleaks protect`, but that doesn't seem to work for `WRN`
+level leaks, which in my experience often contain sensitive cleartext. We can
+use `git-filter-repo` to purge the cleartext from our repo history.
+
+Let's make a `secrets.txt` file that we can feed `git-filter-repo`:
+
+```shell
+λ gitleaks detect -r /tmp/secrets.json
+λ jq -r 'map_values(.Secret) | .[]' /tmp/secrets.txt
+```
+
+Now for the redacting...
+
+```shell
+λ git-filter-repo --force --replace-text /tmp/secrets.txt
+```
+
+Verify that the secrets were removed.
+
+```shell
+λ rg --hidden '\*\*\*REMOVED\*\*\*'
+λ gitleaks detect -v
+```
+
+Looks good! Let's move on to support the adopted repo's deploy strategy.
+
+## Supporting Deploys
+
+While deploying services when someone pushes to a given branch is a common
+deployment strategy, branch-based deployment don't make a whole lot of sense in
+a monorepo.
+
+When adopting another repo, you'll typically encounter a Github Action
+configuration that contains a section like this:
+
+```yaml
+on:
+  push:
+    - staging
+    - production
+```
+
+In our monorepo, `staging` and `production` don't exist. And I don't think we
+want to support them either. `staging` and `production` are ambiguous in a
+monorepo that hosts multiple services each of which likely having its own notion
+of `staging` and `production`.
+
+Doing "pinned releases" where a service is deployed from a `git` revision from
+the mainline branch works well in these scenarios. In order to support this we
+need to make sure the adopted repo has references to
+
+`git subtree add` asks us to define which branch it should use when grafting the
+repository onto our monorepo. We'll use `main` (or whatever the mainline branch
+is).
+
+In order to support the *current* deployments while migrating to a pinned
+release strategy, we have to ensure that `main` has a commit containing the same
+tree state as `staging` *and* another commit containing the same tree state as
+`production`. Let's do that!
+
+```shell
+λ git checkout main # ensure you're on the main branch
+λ git diff main staging >/tmp/main-to-staging.patch
+λ git diff main production >/tmp/main-to-production.patch
+```
+
+### staging
+
+```shell
+λ git apply /tmp/main-to-staging.patch
+λ git add . && git commit # chore: main -> staging
+λ git revert HEAD
+λ git commit --amend # revert: staging -> main
+```
+
+### production
+
+```shell
+λ git apply /tmp/main-to-production.patch
+λ git add . && git commit # chore: main -> production
+λ git revert HEAD
+λ git commit --amend # revert: production -> main
+```
+
+Now let's check our work:
+
+```shell
+λ git log --oneline
+38f4422 revert: production -> main
+f071a9f chore: main -> production
+02ea731 revert: staging -> main
+308ed90 chore: main -> staging
+```
+
+When we go to support pinned releases we can do something like so:
+
+```json
+{
+  "staging": "308ed90",
+  "production": "f071a9f"
+}
+```
+
+## Subtree Merge
+
+Now the repo is ready to be merged.
+
+```shell
+λ git subtree add --prefix=foo/bar/baz path/to/baz main
+λ git commit --amend # subtree: Dock baz into monorepo!
+```
+
+## Formatting
+
+Some CI enforces code formatting standards, so you may need to run that:
+
+```shell
+λ repofmt
+λ git add . && git commit # chore(fmt): Format the codes
+```
+
+Lastly, if you need the latest monorepo code from `origin/main` before opening a
+pull request, the following should work:
+
+```shell
+λ git fetch origin main && git rebase origin/main --rebase-merges --strategy=subtree
+```
diff --git a/users/wpcarro/website/blog/posts/nginx-curl-note.md b/users/wpcarro/website/blog/posts/nginx-curl-note.md
new file mode 100644
index 000000000000..e2f4341f5400
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/nginx-curl-note.md
@@ -0,0 +1,5 @@
+Use the following to make requests to Nginx virtual hosts from the host itself:
+
+```shell
+$ curl -H 'Host: trace.website.internal' localhost:8000
+```
diff --git a/users/wpcarro/website/blog/posts/nix-env-note.md b/users/wpcarro/website/blog/posts/nix-env-note.md
new file mode 100644
index 000000000000..8683c52e8fec
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/nix-env-note.md
@@ -0,0 +1,33 @@
+## Background
+
+Much in the same vain as my [nix-shell (note to self)][nix-shell-note], I'm
+going to leave a note to my future self on how to install packages using
+`nix-env`, which is something I do once in a blue moon.
+
+## Solution
+
+```shell
+λ nix-env -iA tvix.eval -f /depot
+```
+
+Looks like I was forgetting the `-f /depot` option all this time:
+
+> --file / -f path
+>     Specifies the Nix expression (designated below as the active Nix
+>     expression) used by the --install, --upgrade, and --query --available
+>     operations to obtain derivations. The default is ~/.nix-defexpr.
+> - `man nix-env`
+
+## Failed Attempts (don't try these at home)
+
+This section is brought to you by my shell's `Ctrl-r`!
+
+```shell
+λ nix-env -I depot=/depot -iA depot.tvix.eval
+λ NIX_PATH=depot=/depot nix-env -iA depot.tvix.eval
+λ nix-env -iE '(import /depot {}).tvix.eval'
+```
+
+Thanks for reading!
+
+[nix-shell-note]: https://billandhiscomputer.com/blog/posts/nix-shell.html
diff --git a/users/wpcarro/website/blog/posts/nix-shell-note.md b/users/wpcarro/website/blog/posts/nix-shell-note.md
new file mode 100644
index 000000000000..da33c846ceaa
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/nix-shell-note.md
@@ -0,0 +1,50 @@
+## Background
+
+I rarely use `nix-shell` for its originally intended purpose of "reproducing the
+environment of a derivation for development". Instead, I often use it to put
+some executable on my `PATH` for some ad hoc task.
+
+What's `nix-shell`'s "intended purpose"? Let's ask The Man (`man nix-shell`):
+
+> The command nix-shell will build the dependencies of the specified derivation,
+> but not the derivation itself. It will then start an interactive shell in
+> which all environment variables defined by the derivation path have been set
+> to their corresponding values, and the script $stdenv/setup has been
+> sourced. This is useful for reproducing the environment of a derivation for
+> development.
+
+Because I'm abusing `nix-shell` in this way, I'm liable to forget that
+`nix-shell` puts `buildInputs` on `PATH` and *not* the derivation itself. But I
+often only want the derivation!
+
+## Solution
+
+Pass the Nix expression to `nix-shell -p`:
+
+```shell
+λ nix-shell -p '(import /depot {}).tvix.eval'
+```
+
+## Explanation
+
+This works because Nix forwards the arguments passed to `-p` (i.e. `--packages`)
+and interpolates them into this expression here: [source][nix-src]
+
+```nix
+{ ... }@args:
+
+with import <nixpkgs> args;
+
+(pkgs.runCommandCC or pkgs.runCommand) "shell" {
+  buildInputs = [
+    # --packages go here
+  ];
+}
+```
+
+So really you can pass-in *any* valid Nix expression that produces a derivation
+and `nix-shell` will put its outputs on your `PATH`.
+
+Enjoy!
+
+[nix-src]: https://sourcegraph.com/github.com/NixOS/nix@3ae9467d57188f9db41f85b0e5c41c0c9d141955/-/blob/src/nix-build/nix-build.cc?L266
diff --git a/users/wpcarro/website/blog/posts/nixos-disk-full-note.md b/users/wpcarro/website/blog/posts/nixos-disk-full-note.md
new file mode 100644
index 000000000000..4bbd3f58e2fb
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/nixos-disk-full-note.md
@@ -0,0 +1,113 @@
+## Background
+
+Every now and then NixOS hosts runs out of disk space. This happened to my IRC
+server recently...
+
+> No problem. Let's free-up some space with Nix's garbage-collection:
+> - me
+
+```shell
+λ nix-collect-garbage -d # failed due lack of disk space
+```
+
+Ironically Nix needs to do an SQLite transaction before deleting stuff and
+SQLite can't do that if there's no space. This is especially funny because the
+SQLite is probably a `DELETE`.
+
+## Solution
+
+First let's verify that our disk is indeed at capacity:
+
+```shell
+λ df -h
+Filesystem                Size  Used Avail Use% Mounted on
+devtmpfs                  399M     0  399M   0% /dev
+tmpfs                     3.9G     0  3.9G   0% /dev/shm
+tmpfs                     2.0G  3.7M  2.0G   1% /run
+tmpfs                     3.9G  408K  3.9G   1% /run/wrappers
+/dev/disk/by-label/nixos  9.9G  9.9G    0G 100% /
+tmpfs                     4.0M     0  4.0M   0% /sys/fs/cgroup
+tmpfs                     797M     0  797M   0% /run/user/0
+```
+
+Looks like `/dev/disk/by-label/nixos` is at `100%`. Now let's find some easy
+targets to free-up space so that we can run `nix-collect-garbage -d`...
+
+```shell
+λ du -hs /* 2>/dev/null
+8.0K    /bin
+12M     /boot
+0       /dev
+200K    /etc
+68K     /home
+16K     /lost+found
+9.0G    /nix
+0       /proc
+1.2M    /root
+2.9M    /run
+4.0K    /srv
+0       /sys
+44K     /tmp
+12K     /usr
+1.2G    /var
+```
+
+Okay: `/var` looks like an easy candidate. Let's recurse into that directory:
+
+```shell
+λ du -hs /var/*
+40K     /var/cache
+12K     /var/db
+4.0K    /var/empty
+4.0K    /var/google-users.d
+211M    /var/lib
+0       /var/lock
+918M    /var/log
+0       /var/run
+4.0K    /var/spool
+44K     /var/tmp
+λ du -hs /var/log/* # /var/log looks promising
+60M     /var/log/btmp
+82M     /var/log/btmp.1
+776M    /var/log/journal # ah-ha! journald. Let's clean-up some logs
+8.0K    /var/log/lastlog
+1.1M    /var/log/nginx
+4.0K    /var/log/private
+12K     /var/log/wtmp
+```
+
+To retain at most 1w's worth of logs:
+
+```shell
+λ journalctl --vacuum-time=1w
+```
+
+...or if you'd prefer to retain only 100M's worth of logs:
+
+```shell
+λ journalctl --vacuum-size=100M
+```
+
+Now Nix should be able to garbage-collect!
+
+```shell
+λ nix-collect-garbage -d
+```
+
+And lastly verify that it WAI'd:
+
+```
+λ df -h
+Filesystem                Size  Used Avail Use% Mounted on
+devtmpfs                  399M     0  399M   0% /dev
+tmpfs                     3.9G     0  3.9G   0% /dev/shm
+tmpfs                     2.0G  3.7M  2.0G   1% /run
+tmpfs                     3.9G  408K  3.9G   1% /run/wrappers
+/dev/disk/by-label/nixos  9.9G  5.1G  4.3G  55% /
+tmpfs                     4.0M     0  4.0M   0% /sys/fs/cgroup
+tmpfs                     797M     0  797M   0% /run/user/0
+```
+
+## Closing Thoughts
+
+Why doesn't Nix just reserve enough space to be able to GC itself? Not sure...
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 000000000000..dd74387f8bf0
--- /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/restic.md b/users/wpcarro/website/blog/posts/restic.md
new file mode 100644
index 000000000000..4af1fab3682f
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/restic.md
@@ -0,0 +1,91 @@
+Continuing along the trend that [Profpatsch][2] recently inspired in me: writing
+short notes to myself instead of fully fledged blog posts aimed at some
+unknowable audience. Today we're looking at how I burned myself by only
+*partially* RTFD.
+
+## Background
+
+I recently started using [restic][4] and NixOS thanks to the help of [TVL's
+`restic.nix` module][1]. I setup `1x/h` backups to [MinIO][3] (S3-compatible
+storage) for just a handful of `/var/lib` directories (`~9GiB` total), but after
+a few days MinIO reported that my bucket size was `O(100GiB)`!
+
+> What's going on?
+> -- me
+
+```shell
+$ restic stats
+repository 763bfe37 opened successfully, password is correct
+scanning...
+Stats in restore-size mode:
+Snapshots processed:   175
+   Total File Count:   31369384
+         Total Size:   21.027 GiB
+```
+
+> Wait: 20GiB... wat?
+> -- me (moments later)
+
+Maybe we're snapshotting our MinIO buckets, and that's contributing to our
+bucket size. Checking the logs proved that `restic` was backing-up `1.5GiB/h`,
+which supported MinIO's reports.
+
+> Ah maybe `restic stats` isn't reporting what I *think* it's reporting...
+> -- me (again)
+
+Let's consult Le Docs:
+
+```shell
+$ restic stats -h
+
+The "stats" command walks one or multiple snapshots in a repository
+and accumulates statistics about the data stored therein. It reports
+on the number of unique files and their sizes, according to one of
+the counting modes as given by the --mode flag.
+
+It operates on all snapshots matching the selection criteria or all
+snapshots if nothing is specified. The special snapshot ID "latest"
+is also supported. Some modes make more sense over
+just a single snapshot, while others are useful across all snapshots,
+depending on what you are trying to calculate.
+
+[to be continued]
+```
+
+This is where I stopped reading (the first time). But then I returned a second
+time as I was running low on theories...
+
+```shell
+[continued]
+
+The modes are:
+
+* restore-size: (default) Counts the size of the restored files.
+* files-by-contents: Counts total size of files, where a file is
+   considered unique if it has unique contents.
+* raw-data: Counts the size of blobs in the repository, regardless of
+  how many files reference them.
+* blobs-per-file: A combination of files-by-contents and raw-data.
+```
+
+Bingo: `--mode=raw-data` **not** `--mode=restore-size`.
+
+## Solution
+
+```shell
+$ restic stats --mode=raw-data
+repository 763bfe37 opened successfully, password is correct
+scanning...
+Stats in raw-data mode:
+Snapshots processed:   175
+   Total Blob Count:   710988
+         Total Size:   303.216 GiB
+```
+
+> Ah... the world agrees again.
+> -- me
+
+[1]: https://cs.tvl.fyi/depot@2ec0d3611960b163a7052e8554ba065f3c89a8cc/-/blob/ops/modules/restic.nix?L9
+[2]: https://github.com/profpatsch
+[3]: https://min.io/
+[4]: https://restic.net/
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 000000000000..5d18935c7aef
--- /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/blog/posts/ssh-oddities.md b/users/wpcarro/website/blog/posts/ssh-oddities.md
new file mode 100644
index 000000000000..ae0bd5c9f507
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/ssh-oddities.md
@@ -0,0 +1,59 @@
+## Background
+
+I was trying to debug a service over `ssh` that offered password-only
+authentication, but I couldn't seem to get the `ssh` client to prompt me for the
+password.
+
+It looked something like this (skip ahead to the conclusion if you're pressed
+for time):
+
+## Troubleshooting
+
+```shell
+λ ssh root@[redacted]
+Unable to negotiate with [redacted] port 22: no matching host key type found. Their offer: ssh-rsa
+```
+
+But the same command was working just fine for my coworker.
+
+I took a closer look with `ssh -v root@[redacted]`, but nothing jumped-out at
+me. Maybe it's something with *my* `ssh` configuration; let's remove that
+variable:
+
+```shell
+λ ssh -F /dev/null root@[redacted]
+Unable to negotiate with [redacted] port 22: no matching host key type found. Their offer: ssh-rsa
+```
+
+> Ah it looks like there's a way to set my preferred authentication method...
+> -- me
+
+```shell
+λ ssh -F /dev/null -o PreferredAuthentications=password root@[redacted]
+Unable to negotiate with [redacted] port 22: no matching host key type found. Their offer: ssh-rsa
+```
+
+## Conclusion
+
+Well it turns-out that newer SSH clients disable the `ssh-rsa` public key
+signature algorithm because it depends on SHA-1, which is considered insecure.
+
+```shell
+λ ssh -V
+OpenSSH_9.0p1, OpenSSL 1.1.1p  21 Jun 2022
+```
+
+...and according to the `ssh -v` output, the server is running a pre-COVID(!!!)
+version of `ssh`:
+
+```
+debug1: Remote protocol version 2.0, remote software version dropbear_2018.76
+```
+
+So if you don't have time to upgrade the SSH server, and you just want to
+connect, the following should work because we're *opting-into* the less secure
+option:
+
+```shell
+λ ssh -o HostKeyAlgorithms=+ssh-rsa root@[redacted]
+```
diff --git a/users/wpcarro/website/blog/posts/tcp-tunneling-note.md b/users/wpcarro/website/blog/posts/tcp-tunneling-note.md
new file mode 100644
index 000000000000..06f6469aff3d
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/tcp-tunneling-note.md
@@ -0,0 +1,63 @@
+## Background
+
+Let's say we'd like to debug a remote machine but use some of the debugging
+tools we have on our local machine like wireshark.
+
+You *can* run `tcpdump` on the remote and then `scp` the file to your local
+machine to analyze the traffic, but after doing that a few times you may want a
+workflow with a tighter feedback loop. For this we'll forward traffic from a
+remote machine to our local machine.
+
+**Note:** There's also `termshark`, which is a `wireshark` TUI that you can run
+on the remote. It's quite cool!
+
+## Local
+
+Run the following on your local machine to forward your remote's traffic:
+
+```shell
+λ ssh -R 4317:127.0.0.1:4317 -N -f user@remote
+```
+
+Here is an abridged explanation of the flags we're passing from `man ssh`:
+
+```
+-N     Do  not  execute  a remote command.  This is useful for just forwarding ports.
+-f     Requests ssh to go to background just before command execution.
+```
+
+**Note:** I couldn't find a good explanation for the `-R` option, so I tried
+removing it and re-running the command, but that results in a resolution error:
+
+```
+ssh: Could not resolve hostname 4317:127.0.0.1:4317: Name or service not known
+```
+
+The remote should now be forwarding traffic from port `4317` to our
+machine.
+
+## Testing
+
+Let's generate some traffic on the remote:
+
+```shell
+λ telnet localhost 4317
+Trying ::1...
+Connected to localhost.
+Escape character is '^]'.
+hello
+world
+```
+
+Locally you should see:
+
+```shell
+λ nc -l 4317 -k # run this *before* running the above command
+hello
+world
+```
+
+You should now be able to `tcpdump -i lo port 4317` or just use `wireshark`
+locally.
+
+Happy debugging!
diff --git a/users/wpcarro/website/blog/posts/tee-time.md b/users/wpcarro/website/blog/posts/tee-time.md
new file mode 100644
index 000000000000..c8107fcded69
--- /dev/null
+++ b/users/wpcarro/website/blog/posts/tee-time.md
@@ -0,0 +1,16 @@
+I encountered this fun TIL while troubleshooting Linux write permissions
+issues...
+
+## TL;DR
+
+Don't do this (unless you want misleading test results):
+
+```shell
+λ sudo -u node-exporter echo 'Hello, world' >/var/lib/textfile-exporter/test.prom
+```
+
+Do this:
+
+```shell
+λ echo 'Hello, world' | sudo -u node-exporter tee /var/lib/textfile-exporter/test.prom
+```
diff --git a/users/wpcarro/website/default.nix b/users/wpcarro/website/default.nix
new file mode 100644
index 000000000000..56f5b02cc89d
--- /dev/null
+++ b/users/wpcarro/website/default.nix
@@ -0,0 +1,77 @@
+{ 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;
+  };
+
+  # Create a simple static file server using nginx to serve `content`.
+  nginxCfgFor = content: pkgs.writeText "nginx.conf" ''
+    user nobody nobody;
+    daemon off;
+    error_log /dev/stdout info;
+    pid /dev/null;
+    events {}
+    http {
+      server {
+        listen 8080;
+        location / {
+          root ${content};
+        }
+      }
+    }
+  '';
+in
+rec {
+  inherit domain renderTemplate withBrand;
+
+  content = pkgs.runCommand "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
+  '';
+
+  # Create a docker image suitable for Google Cloud Run (to save costs).
+  image = pkgs.dockerTools.buildLayeredImage {
+    name = "website";
+    tag = "latest";
+    contents = [ pkgs.fakeNss ];
+    extraCommands = ''
+      mkdir -p tmp/nginx_client_body
+      mkdir -p var/log/nginx
+    '';
+    config = {
+      Cmd = [ "${pkgs.nginx}/bin/nginx" "-c" (nginxCfgFor content) ];
+      ExposedPorts = { "8080/tcp" = { }; };
+    };
+  };
+
+  meta.ci.targets = [ "root" "image" ];
+}
diff --git a/users/wpcarro/website/fragments/.skip-subtree b/users/wpcarro/website/fragments/.skip-subtree
new file mode 100644
index 000000000000..e69de29bb2d1
--- /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 000000000000..79ccd0ded432
--- /dev/null
+++ b/users/wpcarro/website/fragments/homepage.html
@@ -0,0 +1,20 @@
+<section class="leading-6 md:leading-7 text-xs md:text-base">
+  <p class="relative text-xl md: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 000000000000..0da4e46d650f
--- /dev/null
+++ b/users/wpcarro/website/fragments/template.html
@@ -0,0 +1,96 @@
+<!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-3 md:py-6">
+      <div class="flex max-w-sm md:max-w-3xl mx-auto">
+        <div class="flex-1 text-center md:text-left text-sm 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>
+        </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 000000000000..a4a62da526d3
--- /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 000000000000..0c1c258f65a4
--- /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 000000000000..506cdf9c4ac8
--- /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 000000000000..3036ba1821cb
--- /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 000000000000..f16361ac4358
--- /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 000000000000..7f6f77741a9c
--- /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 000000000000..6839ac4fabdc
--- /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 000000000000..b5c61c956711
--- /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 000000000000..b587e0901284
--- /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 000000000000..b522419aa3b7
--- /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 000000000000..d2671b2cf17a
--- /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 000000000000..afcc0f4d3645
--- /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 000000000000..691adc939464
--- /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 000000000000..2ddedb913357
--- /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 000000000000..ea00a013513e
--- /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 000000000000..5b5426913570
--- /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 000000000000..23b13c224c68
--- /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 000000000000..e9d0356aaa63
--- /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 000000000000..fdf1c6188a4c
--- /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 000000000000..9bd6fc914bb6
--- /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 000000000000..ce7e534b234b
--- /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 000000000000..3530bef763c9
--- /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 000000000000..a23795075b11
--- /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 000000000000..a3ae929ef446
--- /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 000000000000..288f03321804
--- /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 000000000000..02ebc92b68df
--- /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 000000000000..b5c61c956711
--- /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 000000000000..91752af916a4
--- /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 000000000000..dc28dc4a9cc8
--- /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 000000000000..b02053d302b6
--- /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 000000000000..3da6fa0dc7b7
--- /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 000000000000..fe07ec1da4d4
--- /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 000000000000..45fdea32b7da
--- /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 000000000000..309b1fa64b4d
--- /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 000000000000..15769f7490e0
--- /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 000000000000..939506c8cce1
--- /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 000000000000..f918c4033e2f
--- /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 000000000000..1f48906d8ffc
--- /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 000000000000..af829e20f9cb
--- /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 000000000000..3e66831c9e82
--- /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 000000000000..d7b4940a55b9
--- /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 000000000000..2af860014378
--- /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 000000000000..ecd5475af266
--- /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 000000000000..a4a62da526d3
--- /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 000000000000..0c1c258f65a4
--- /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 000000000000..2527f4b96353
--- /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 000000000000..7cfdf7c45137
--- /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 000000000000..c62262e6835a
--- /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 000000000000..a95f80408ec4
--- /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 000000000000..4c2372280ed5
--- /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 000000000000..b5c61c956711
--- /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 000000000000..5687c29eb7d0
--- /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 000000000000..b522419aa3b7
--- /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 000000000000..a73307ccda04
--- /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 000000000000..afcc0f4d3645
--- /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 000000000000..a4917529392a
--- /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 000000000000..2c8626b09293
--- /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 000000000000..b066fb2f6f92
--- /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 000000000000..288d7a825f4b
--- /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 000000000000..628b52d79da9
--- /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 000000000000..d231f1467438
--- /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 000000000000..5d87bcee501e
--- /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 000000000000..59e6c8234c13
--- /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 000000000000..5d97161df6a8
--- /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 000000000000..678fb0f9aa79
--- /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 000000000000..57d419db5a82
--- /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 000000000000..041313614f53
--- /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 000000000000..7f54832c97a0
--- /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 000000000000..a6876c4f8a0d
--- /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 000000000000..9e714732fe85
--- /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 000000000000..151d070ca1a4
--- /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 000000000000..fe3671e7aa3e
--- /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 000000000000..b7a31457b79e
--- /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 000000000000..7c153e422822
--- /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 000000000000..dcccadcb7022
--- /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 000000000000..228c3363bc59
--- /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 000000000000..3c476bbf7b87
--- /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 000000000000..5370b90abebf
--- /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 000000000000..24054bf47afd
--- /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 000000000000..4a72865153ab
--- /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 000000000000..2f401af2fb8f
--- /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 000000000000..262693ae821e
--- /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 000000000000..459a5c8c8250
--- /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 000000000000..c220bd440636
--- /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 000000000000..6ec8264470db
--- /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 000000000000..9efe53eccdbb
--- /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/ynabsql/dataviz/.gitignore b/users/wpcarro/ynabsql/dataviz/.gitignore
new file mode 100644
index 000000000000..efb13a154957
--- /dev/null
+++ b/users/wpcarro/ynabsql/dataviz/.gitignore
@@ -0,0 +1,5 @@
+/dist
+/node_modules
+/.parcel-cache
+/cdn
+/data.js
\ No newline at end of file
diff --git a/users/wpcarro/ynabsql/dataviz/.parcelrc b/users/wpcarro/ynabsql/dataviz/.parcelrc
new file mode 100644
index 000000000000..5dacc3dd88b6
--- /dev/null
+++ b/users/wpcarro/ynabsql/dataviz/.parcelrc
@@ -0,0 +1,3 @@
+{
+    "extends": "@parcel/config-default"
+}
\ No newline at end of file
diff --git a/users/wpcarro/ynabsql/dataviz/cdn b/users/wpcarro/ynabsql/dataviz/cdn
new file mode 120000
index 000000000000..9c83dcee432a
--- /dev/null
+++ b/users/wpcarro/ynabsql/dataviz/cdn
@@ -0,0 +1 @@
+/tmp/cdn
\ No newline at end of file
diff --git a/users/wpcarro/ynabsql/dataviz/components.jsx b/users/wpcarro/ynabsql/dataviz/components.jsx
new file mode 100644
index 000000000000..0dbfc9fd801b
--- /dev/null
+++ b/users/wpcarro/ynabsql/dataviz/components.jsx
@@ -0,0 +1,1256 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import Chart from 'chart.js/auto';
+import 'chartjs-adapter-date-fns';
+
+const colors = {
+    red: 'rgb(255, 45, 70)',
+    green: 'rgb(75, 192, 35)',
+    white: 'rgb(249, 246, 238)',
+    blue: 'rgb(137, 207, 240)',
+    fadedBlue: 'rgb(137, 207, 240, 0.25)',
+    purple: 'rgb(203, 195, 227)',
+    brown: 'rgb(205, 127, 50)',
+    black: 'rgb(53, 57, 53)',
+};
+
+const months = [
+    'January',
+    'February',
+    'March',
+    'April',
+    'May',
+    'June',
+    'July',
+    'August',
+    'September',
+    'October',
+    'November',
+    'December',
+];
+
+function getWeek(x) {
+    const dowOffset = 0;
+    const newYear = new Dte(x.getFullYear(), 0, 1);
+    let day = newYear.getDay() - dowOffset; //the day of week the year begins on
+    day = (day >= 0 ? day : day + 7);
+    const daynum = Math.floor((x.getTime() - newYear.getTime() -
+        (x.getTimezoneOffset() - newYear.getTimezoneOffset()) * 60000) / 86400000) + 1;
+    let weeknum;
+
+    //if the year starts before the middle of a week
+    if (day < 4) {
+        weeknum = Math.floor((daynum + day - 1) / 7) + 1;
+        if (weeknum > 52) {
+            const nYear = new Date(x.getFullYear() + 1, 0, 1);
+            let nday = nYear.getDay() - dowOffset;
+            nday = nday >= 0 ? nday : nday + 7;
+            /*if the next year starts before the middle of
+              the week, it is week #1 of that year*/
+            weeknum = nday < 4 ? 1 : 53;
+        }
+    }
+    else {
+        weeknum = Math.floor((daynum + day - 1) / 7);
+    }
+    return weeknum;
+}
+
+// Convert a sorting expressions (e.g. "Outflow DESC; Date ASC; Category ASC")
+// into a function that can be passed to Array.prototype.sort.
+function compileSort(expr) {
+    return expr.split(/\s*;\s*/).reverse().reduce((acc, x) => {
+        const [k, dir] = x.split(/\s+/);
+        if (dir === 'ASC') {
+            return function(x, y) {
+                if (x[k] > y[k]) { return 1; }
+                if (x[k] < y[k]) { return -1; }
+                else { return acc(x, y); }
+            };
+        }
+        if (dir === 'DESC') {
+            return function(x, y) {
+                if (x[k] > y[k]) { return -1; }
+                if (x[k] < y[k]) { return 1; }
+                else { return acc(x, y); }
+            };
+        }
+        else {
+            throw new Error(`Sort direction not supported, ${dir}, must be either "ASC" or "DESC"`);
+        }
+    }, function(x, y) { return 0; })
+}
+
+function dollars(n, sensitive) {
+    if (sensitive) {
+        const order = magnitude(n);
+        // Shortcut to avoid writing comma-insertion logic v0v.
+        if (n === 0) {
+            return '$X.XX';
+        }
+        if (order <= 0) {
+            return '$X.XX';
+        }
+        if (order === 1) {
+            return '$XX.XX';
+        }
+        if (order === 2) {
+            return '$XXX.XX';
+        }
+        if (order === 3) {
+            return '$X,XXX.XX';
+        }
+        if (order === 4) {
+            return '$XX,XXX.XX';
+        }
+        if (order === 4) {
+            return '$XX,XXX.XX';
+        }
+        if (order === 5) {
+            return '$XXX,XXX.XX';
+        }
+        // Coming soon! :P
+        if (order === 6) {
+            return '$X,XXX,XXX.XX';
+        }
+        if (order === 7) {
+            return '$XX,XXX,XXX.XX';
+        }
+        if (order === 8) {
+            return '$XXX,XXX,XXX.XX';
+        }
+        // Unsupported
+        else {
+            return '$???.??';
+        }
+    }
+    return usd.format(n);
+}
+
+const usd = new Intl.NumberFormat('en-US', {
+    style: 'currency',
+    currency: 'USD',
+});
+
+const queries = {
+    housing: 'Category:/(rent|electric)/',
+    food: 'Category:/(eating|alcohol|grocer)/',
+    commute: 'Category:/(vacation|gasoline|parking|car maintenance)/',
+};
+
+/**
+ * Return the Order of Magnitude of some value, x.
+ */
+function magnitude(x) {
+    return Math.floor(Math.log(x) / Math.LN10 + 0.000000001);
+}
+
+function getSum(transactions) {
+    return transactions.reduce((acc, x) => acc + x.Outflow, 0);
+}
+
+function transactionKey(x) {
+    const keys = [
+        'Account',
+        'Flag',
+        'Date',
+        'Payee',
+        'Category',
+        'Memo',
+        'Outflow',
+        'Inflow',
+        'Cleared',
+    ];
+    return keys.map(k => x[k]).join('|');
+}
+
+function parseCSV(csv) {
+  let lines=csv.split("\n");
+  let result = [];
+
+  // Strip the surrounding 2x-quotes from the header.
+  //
+  // NOTE: If your columns contain commas in their values, you'll need
+  // to deal with those before doing the next step
+  let headers = lines[0].split(",").map(x => x.slice(1, -1));
+
+  for(let i = 1; i < lines.length; i += 1) {
+      let obj = {};
+      let currentline=lines[i].split(",");
+
+      for(let j = 0; j < headers.length; j += 1) {
+        obj[headers[j]] = currentline[j].slice(1, -1);
+      }
+
+      result.push(obj);
+  }
+
+  return result.map(x => ({
+    ...x,
+    Date: new Date(x.Date),
+    Inflow: parseFloat(x.Inflow),
+    Outflow: parseFloat(x.Outflow),
+  }));
+}
+
+
+class UploadJSON extends React.Component {
+    handleUpload(e) {
+        let files = e.target.files;
+        if (!files.length) {
+            alert('No file selected!');
+            return;
+        }
+        let file = files[0];
+        let reader = new FileReader();
+        reader.onload = (event) => {
+            this.props.onUpload(parseCSV(event.target.result));
+        };
+        reader.readAsText(file);
+    }
+    render() {
+        return <input onChange={e => this.handleUpload(e)} id="json-upload" type="file" accept="application/csv" />;
+    }
+}
+
+class ScatterChart extends React.Component {
+    constructor(props) {
+        super(props);
+        this.chart = null;
+        // Generate a 1/1M random ID.
+        this.id = btoa(Math.floor(Math.random() * 1e9));
+    }
+    componentDidUpdate(prevProps) {
+        if (this.props.transactions !== prevProps.transactions) {
+            this.chart.data.datasets[0].data = this.props.transactions.filter(x => x.Inflow > 0).map(x => ({
+                x: x.Date,
+                y: x.Inflow,
+                metadata: x,
+            }));
+            this.chart.data.datasets[1].data = this.props.transactions.filter(x => x.Outflow > 0).map(x => ({
+                x: x.Date,
+                y: x.Outflow,
+                metadata: x,
+            }));
+            this.chart.update();
+        }
+    }
+    componentDidMount() {
+        const mount = document.getElementById(this.id);
+        this.chart = new Chart(mount, {
+            type: 'scatter',
+            data: {
+                datasets: [
+                    {
+                        label: 'Revenue',
+                        data: this.props.transactions.filter(x => x.Inflow > 0).map(x => ({
+                            x: x.Date,
+                            y: x.Inflow,
+                            metadata: x,
+                        })),
+                        backgroundColor: colors.green,
+                    },
+                    {
+                        label: 'Expenses',
+                        data: this.props.transactions.filter(x => x.Outflow).map(x => ({
+                            x: x.Date,
+                            y: x.Outflow,
+                            metadata: x,
+                        })),
+                        backgroundColor: colors.red,
+                    },
+                ],
+            },
+            options: {
+                scales: {
+                    x: {
+                        type: 'time',
+                        title: {
+                            display: true,
+                            text: 'Date',
+                        },
+                    },
+                    y: {
+                        title: {
+                            display: true,
+                            text: 'Amount ($USD)'
+                        },
+                    },
+                },
+                plugins: {
+                    tooltip: {
+                        callbacks: {
+                            title: function (x) {
+                                return `$${x[0].raw.y} (${x[0].raw.metadata.Date.toLocaleDateString()})`;
+                            },
+                            label: function (x) {
+                                const { Category, Payee, Memo } = x.raw.metadata;
+                                return `${Payee} - ${Category} (${Memo})`;
+                            },
+                        },
+                    },
+                },
+            },
+        });
+    }
+    render() {
+        return <canvas id={this.id}></canvas>;
+    }
+}
+
+/**
+ * Generic line chart parameterized by:
+ * - datasets: forwarded to chart.js library
+ * - x: string label for x-axis
+ * - y: string label for y-axis
+ */
+class GenLineChart extends React.Component {
+    constructor(props) {
+        super(props);
+        this.chart = null;
+        // Generate a 1/1M random ID.
+        this.id = btoa(Math.floor(Math.random() * 1e9));
+    }
+
+    componentDidUpdate(prevProps, prevState) {
+        if (this.props.datasets != prevProps.datasets) {
+            this.chart.data.datasets = this.props.datasets;
+            this.chart.update();
+        }
+    }
+
+    componentDidMount() {
+        const mount = document.getElementById(this.id);
+        this.chart = new Chart(mount, {
+            type: 'line',
+            data: {
+                datasets: this.props.datasets,
+            },
+            options: {
+                scales: {
+                    x: {
+                        type: 'time',
+                        title: {
+                            display: true,
+                            text: this.props.x,
+                        },
+                    },
+                    y: {
+                        title: {
+                            display: true,
+                            text: this.props.y
+                        },
+                    },
+                },
+            },
+        });
+    }
+
+    render() {
+        return <canvas id={this.id}></canvas>;
+    }
+}
+
+class DonutChart extends React.Component {
+    constructor(props) {
+        super(props);
+        this.chart = null;
+        // Generate a 1/1M random ID.
+        this.id = btoa(Math.floor(Math.random() * 1e9));
+    }
+
+    componentDidUpdate(prevProps, prevState) {
+        if (this.props.datasets != prevProps.datasets) {
+            this.chart.data.datasets = this.props.datasets;
+            this.chart.update();
+        }
+    }
+
+    componentDidMount() {
+        const mount = document.getElementById(this.id);
+        this.chart = new Chart(mount, {
+            type: 'doughnut',
+            data: {
+                labels: this.props.labels,
+                datasets: this.props.datasets,
+            },
+            options: {
+                resonsive: true,
+            },
+        });
+    }
+
+    render() {
+        return <canvas id={this.id}></canvas>;
+    }
+}
+
+class StackedHistogram extends React.Component {
+    constructor(props) {
+        super(props);
+        this.chart = null;
+        // Generate a 1/1M random ID.
+        this.id = btoa(Math.floor(Math.random() * 1e9));
+    }
+
+    componentDidUpdate(prevProps, prevState) {
+        if (this.props.datasets != prevProps.datasets) {
+            this.chart.data.datasets = this.props.datasets;
+            this.chart.update();
+        }
+    }
+
+    componentDidMount() {
+        const mount = document.getElementById(this.id);
+        this.chart = new Chart(mount, {
+            type: 'bar',
+            data: {
+                labels: this.props.labels,
+                datasets: this.props.datasets,
+            },
+            options: {
+                scales: {
+                    x: {
+                        stacked: true,
+                    },
+                    y: {
+                        stacked: true,
+                    },
+                },
+            },
+        });
+    }
+
+    render() {
+        return <canvas id={this.id}></canvas>;
+    }
+}
+
+/**
+ * Display the "Actual Savings Rate" (bucketed by month) as a line chart with
+ * the "Expected Savings Rate" overlay.
+ */
+class SavingsRateLineChart extends React.Component {
+    constructor(props) {
+        super(props);
+        this.chart = null;
+        // Generate a 1/1M random ID.
+        this.id = btoa(Math.floor(Math.random() * 1e9));
+    }
+
+    static getRevenue(transactions) {
+        // Bucket revenues into months.
+        return transactions.reduce((acc, x) => {
+            const month = x.Date.getMonth();
+            acc[month] += x.Inflow;
+            return acc;
+        }, new Array(12).fill(0));
+    }
+
+    static getExpenses(transactions) {
+        // Bucket revenues into months.
+        return transactions.reduce((acc, x) => {
+            const month = x.Date.getMonth();
+            acc[month] += x.Outflow;
+            return acc;
+        }, new Array(12).fill(0));
+    }
+
+    componentDidMount() {
+        const mount = document.getElementById(this.id);
+        const revenue = SavingsRateLineChart.getRevenue(this.props.transactions);
+        const expenses = SavingsRateLineChart.getExpenses(this.props.transactions);
+
+        this.chart = new Chart(mount, {
+            type: 'line',
+            data: {
+                datasets: [
+                    {
+                        label: 'actual savings (by month)',
+                        data: new Array(12).fill(null).map((_, i) => ({
+                            x: i,
+                            y: (revenue[i] - expenses[i]) / revenue[i],
+                        })),
+                        cubicInterpolationMode: 'monotone',
+                        tension: 0.4,
+                        borderColor: colors.fadedBlue,
+                        backgroundColor: colors.fadedBlue,
+                    },
+                    {
+                        label: 'actual savings (overall)',
+                        data: new Array(12).fill(null).map((_, i) => ({
+                            x: i,
+                            y: this.props.rate,
+                        })),
+                        cubicInterpolationMode: 'monotone',
+                        tension: 0.4,
+                        borderColor: colors.blue,
+                        backgroundColor: colors.blue,
+                    },
+                    // 0% marker (out of debt)
+                    {
+                        label: 'beginner (0%)',
+                        data: new Array(12).fill(null).map((x, i) => ({
+                            x: i,
+                            y: 0.00,
+                        })),
+                        cubicInterpolationMode: 'monotone',
+                        tension: 0.4,
+                        borderColor: colors.white,
+                        backgroundColor: colors.white,
+                    },
+                    // 25% marker (quarter "Washington" club)
+                    {
+                        label: 'healthy (25%)',
+                        data: new Array(12).fill(null).map((x, i) => ({
+                            x: i,
+                            y: 0.25,
+                        })),
+                        cubicInterpolationMode: 'monotone',
+                        tension: 0.4,
+                        borderColor: colors.purple,
+                        backgroundColor: colors.purple,
+                    },
+                    // 50% marker (1/2-dollar "Kennedy" club)
+                    {
+                        label: 'rich (50%)',
+                        data: new Array(12).fill(null).map((x, i) => ({
+                            x: i,
+                            y: 0.50,
+                        })),
+                        cubicInterpolationMode: 'monotone',
+                        tension: 0.4,
+                        borderColor: colors.brown,
+                        backgroundColor: colors.brown,
+                    },
+                    // 75% marker
+                    {
+                        label: 'wealthy (75%)',
+                        data: new Array(12).fill(null).map((x, i) => ({
+                            x: i,
+                            y: 0.75,
+                        })),
+                        cubicInterpolationMode: 'monotone',
+                        tension: 0.4,
+                        borderColor: colors.black,
+                        backgroundColor: colors.black,
+                    },
+                ],
+                labels: months,
+            },
+            options: {
+                scales: {
+                    y: {
+                        max: 1.0,
+                        min: -1.0,
+                        title: {
+                            display: true,
+                            text: 'Savings Rate (%)'
+                        },
+                    },
+                },
+            },
+        });
+    }
+
+    componentDidUpdate(prevProps, prevState) {
+        // Bucket revenues into months.
+        const revenue = SavingsRateLineChart.getRevenue(this.props.transactions);
+        const expenses = SavingsRateLineChart.getExpenses(this.props.transactions);
+
+        this.chart.data.datasets[0].data = new Array(12).fill(null).map((_, i) => ({
+            x: i,
+            y: (revenue[i] - expenses[i]) / revenue[i],
+        }));
+        this.chart.update();
+    }
+
+    render() {
+        return <canvas id={this.id}></canvas>;
+    }
+}
+
+class App extends React.Component {
+    constructor(props) {
+        super(props);
+        const query = 'Account:/checking/';
+        const allTransactions = [];
+        const savingsView = 'after:"01/01/2022"';
+        const inflowQuery = 'Account:/checking/';
+        const outflowQuery = 'Account:/checking/ -Category:/(stocks|crypto)/';
+
+        // slx configuration
+        const slxCaseSensitive = false;
+        const slxPreferRegex = true;
+        const slxDateKey = 'Date';
+
+        this.state = {
+            query,
+            transactionsView: 'table',
+            sensitive: false,
+            allTransactions,
+            slxCaseSensitive,
+            slxPreferRegex,
+            slxDateKey,
+            filteredTransactions: select(query, allTransactions, {
+                caseSensitive: slxCaseSensitive,
+                preferRegex: slxPreferRegex,
+                dateKey: slxDateKey,
+            }),
+            saved: {},
+            focus: {
+                sum: false,
+                1000: false,
+                100: false,
+                10: false,
+                1: false,
+                0.1: false,
+            },
+            budget: [
+                {
+                    label: 'Flexible',
+                    children: [
+                        { label: 'groceries - $200/mo', savings: false, monthly: 400.00 },
+                        { label: 'eating out - $150/mo', savings: false, monthly: 200.00 },
+                        { label: 'alcohol - $200/mo', savings: false, monthly: 200.00 },
+                        { label: 'household items - $50/mo', savings: false, monthly: 50.00 },
+                        { label: 'toiletries - $200/yr', savings: false, monthly: 200.00 / 12 },
+                        { label: 'haircuts - $400/yr', savings: false, monthly: 400.00 / 12 },
+                        { label: 'gasoline - $100/mo', savings: false, monthly: 100.00 },
+                        { label: 'parking - $10/mo', savings: false, monthly: 10.00 },
+                        { label: 'ride services - $25/mo', savings: false, monthly: 50.00 },
+                        { label: 'LMNT - $45/mo', savings: false, monthly: 45.00 },
+                        { label: 'books - $25/mo', savings: false, monthly: 25.00 },
+                        { label: 'vacation - $4,000/yr', savings: false, monthly: 4000.00 / 12 },
+                        { label: 'reimbursements - $5,000 balance', savings: false, monthly: 0.00 },
+                    ],
+                },
+                {
+                    label: 'Fixed',
+                    children: [
+                        { label: 'rent - $3,100/mo', savings: false, monthly: 3100.00 },
+                        { label: 'electric - $50/mo', savings: false, monthly: 50.00 },
+                        { label: 'SoCalGas - $30/mo', savings: false, monthly: 30.00 },
+                        { label: 'YNAB', savings: false, monthly: 100.00 / 12 },
+                        { label: 'Robinhood Gold', savings: false, monthly: 5.00 },
+                        { label: 'Spotify', savings: false, monthly: 10.00 },
+                        { label: 'Surfline', savings: false, monthly: 100.00 / 12 },
+                        { label: 'HBO Max', savings: false, monthly: 170.00 },
+                        { label: 'Clear', savings: false, monthly: 179.00 },
+                        { label: 'car insurance', savings: false, monthly: 100.00 },
+                        { label: 'Making Sense', savings: false, monthly: 50.00 / 12 },
+                        { label: 'internet', savings: false, monthly: 100.00 },
+                        { label: 'tax return', savings: false, monthly: 200.00 / 12 },
+                    ],
+                },
+                {
+                    label: 'Rainy Day (dont touch)',
+                    children: [
+                        { label: 'emergency fund', savings: false, monthly: 0.00 },
+                        { label: 'check-ups', savings: false, monthly: 7.50 },
+                        { label: 'car maintenance', savings: false, monthly: 98.33 },
+                    ],
+                },
+                {
+                    label: 'Savings (dont touch)',
+                    children: [
+                        { label: 'stocks', savings: true, monthly: 4000.00 },
+                        { label: 'crypto', savings: true, monthly: 736.00 },
+                    ],
+                },
+                {
+                    label: 'Gifts (dont touch)',
+                    children: [
+                        { label: 'birthdays', savings: false, monthly: 250.00 / 12 },
+                        { label: 'Valentines Day', savings: false, monthly: 100.00 / 12 },
+                        { label: 'Mothers Day', savings: false, monthly: 25.00 / 12 },
+                        { label: 'Fathers Day', savings: false, monthly: 25.00 / 12 },
+                        { label: 'Christmas', savings: false, monthly: 500.00 / 12 },
+                    ],
+                },
+                {
+                    label: 'Error Budget',
+                    children: [
+                        { label: 'stuff I forgot to budget for - $2,254/mo', savings: false, monthly: 0.00 },
+                    ],
+                },
+            ],
+            paycheck: 6000.00,
+            view: 'query',
+            savingsView,
+            inflowQuery,
+            outflowQuery,
+            inflowTransactions: select(inflowQuery, select(savingsView, allTransactions, {
+                caseSensitive: slxCaseSensitive,
+                preferRegex: slxPreferRegex,
+                dateKey: slxDateKey,
+            }), {
+                caseSensitive: slxCaseSensitive,
+                preferRegex: slxPreferRegex,
+                dateKey: slxDateKey,
+            }),
+            outflowTransactions: select(outflowQuery, select(savingsView, allTransactions, {
+                caseSensitive: slxCaseSensitive,
+                preferRegex: slxPreferRegex,
+                dateKey: slxDateKey,
+            }), {
+                caseSensitive: slxCaseSensitive,
+                preferRegex: slxPreferRegex,
+                dateKey: slxDateKey,
+            }),
+            sortExpr: 'Date DESC; Outflow DESC',
+        };
+    }
+
+    render() {
+        const sum = this.state.filteredTransactions.reduce((acc, { Outflow }) => acc + Outflow, 0);
+        const savedSum = Object.values(this.state.saved).reduce((acc, sum) => acc + sum, 0);
+        const categories = this.state.allTransactions.reduce((acc, x) => {
+            if (!(x.Category in acc)) {
+                acc[x.Category] = [];
+            }
+            acc[x.Category].push(x);
+            return acc;
+        }, {});
+
+        let view = null;
+        if (this.state.view === 'query') {
+            view = (
+                <QueryView
+                    transactionsView={this.state.transactionsView}
+                    sortExpr={this.state.sortExpr}
+                    onSortExprChange={sortExpr => this.setState({ sortExpr })}
+                    sensitive={this.state.sensitive}
+                    query={this.state.query}
+                    focus={this.state.focus}
+                    allTransactions={this.state.allTransactions}
+                    transactions={this.state.filteredTransactions}
+                    saved={this.state.saved}
+                    setState={this.setState.bind(this)}
+                    slxConfig={{
+                        caseSensitive: this.state.slxCaseSensitive,
+                        preferRegex: this.state.slxPreferRegex,
+                        dateKey: this.state.slxDateKey,
+                    }}
+                />
+            );
+        } else if (this.state.view === 'savings') {
+            view = (
+                <SavingsView
+                    categories={categories}
+                    sensitive={this.state.sensitive}
+                    savingsView={this.state.savingsView}
+                    inflowQuery={this.state.inflowQuery}
+                    outflowQuery={this.state.outflowQuery}
+                    inflowTransactions={this.state.inflowTransactions}
+                    outflowTransactions={this.state.outflowTransactions}
+                    onFilterInflow={() => this.setState({
+                        inflowTransactions: select(this.state.inflowQuery, select(this.state.savingsView, this.state.allTransactions, {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }), {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }),
+                    })}
+                    onFilterOutflow={() => this.setState({
+                        outflowTransactions: select(this.state.outflowQuery, select(this.state.savingsView, this.state.allTransactions, {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }), {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }),
+                    })}
+                    onFilterSavingsView={() => this.setState({
+                        inflowTransactions: select(this.state.inflowQuery, select(this.state.savingsView, this.state.allTransactions, {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }), {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }),
+                        outflowTransactions: select(this.state.outflowQuery, select(this.state.savingsView, this.state.allTransactions, {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }), {
+                            caseSensitive: this.state.slxCaseSensitive,
+                            preferRegex: this.state.slxPreferRegex,
+                            dateKey: this.state.slxDateKey,
+                        }),
+                    })}
+                    onSavingsViewChange={x => this.setState({ savingsView: x })}
+                    onInflowQueryChange={x => this.setState({ inflowQuery: x })}
+                    onOutflowQueryChange={x => this.setState({ outflowQuery: x })}
+                />
+            );
+        } else if (this.state.view === 'budget') {
+            // Planned expenses:
+            // - minus planned assignment to emergency fund (not an expense)
+            // - minus planned spend to investments (e.g. stocks, crypto)
+            const budgetedSpend = this.state.budget.reduce((acc, x) => acc + x.children.filter(x => x.savings).reduce((acc, x) => acc + x.monthly, 0), 0);
+
+            view = (
+                <div>
+                    <fieldset>
+                        <legend>Details</legend>
+                        <div className="form-group">
+                            <label htmlFor="paycheck">Paycheck</label>
+                            <input name="paycheck" type="text" />
+                        </div>
+                        <div className="form-group">
+                            <label htmlFor="savings-rate">Savings Rate</label>
+                            <input name="savings-rate" type="text" />
+                        </div>
+                    </fieldset>
+                    <ul>
+                        <li>Available Spend: {dollars(this.state.paycheck * 2, this.state.sensitive)}</li>
+                        <li>Target Spend: {dollars(this.state.paycheck, this.state.sensitive)}</li>
+                        <li>Budgeted Spend (minus savings): {dollars(budgetedSpend, this.state.sensitive)}</li>
+                        <li>Emergency Fund Size (recommended): {dollars(budgetedSpend * 3, this.state.sensitive)}</li>
+                    </ul>
+                    <div style={{ width: '30em' }}>
+                        <DonutChart labels={this.state.budget.map(x => x.label)} datasets={[
+                            {
+                                label: 'Categories',
+                                data: this.state.budget.map(x => x.children.reduce((acc, y) => acc + y.monthly, 0)),
+                            }
+                        ]} />
+                    </div>
+                    <ul>
+                        {this.state.budget.map(x => (
+                            <li>
+                                <div>{x.label} - {dollars(x.children.reduce((acc, x) => acc + x.monthly, 0), this.state.sensitive)}</div>
+                                <table>
+                                    <thead>
+                                        <tr>
+                                            <th>category</th>
+                                            <th>assigned (target)</th>
+                                            <th>last year (actual)</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                        {x.children.map(y => (
+                                            <tr>
+                                                <td>{y.label}</td>
+                                                <td>{dollars(y.monthly, this.state.sensitive)}</td>
+                                                <td>{dollars((categories[y.label] || []).reduce((acc, x) => acc + x.Outflow, 0) / 12, this.state.sensitive)}</td>
+                                            </tr>
+                                        ))}
+                                    </tbody>
+                                </table>
+                            </li>
+                        ))}
+                    </ul>
+                </div>
+            );
+        }
+
+        return (
+            <div className="container">
+                <nav className="terminal-menu">
+                    <ul>
+                        <li><a href="#" onClick={() => this.setState({ view: 'query' })}>query</a></li>
+                        <li><a href="#" onClick={() => this.setState({ view: 'savings' })}>savings</a></li>
+                        <li><a href="#" onClick={() => this.setState({ view: 'budget' })}>budget</a></li>
+                        <li><a href="#" onClick={() => this.setState({ sensitive: !this.state.sensitive })}>sensitive</a></li>
+                    </ul>
+                </nav>
+                <UploadJSON onUpload={xs => this.setState({
+                    allTransactions: xs,
+                    filteredTransactions: select(this.state.query, xs, {
+                        caseSensitive: this.state.slxCaseSensitive,
+                        preferRegex: this.state.slxPreferRegex,
+                        slxDateKey: this.state.slxDateKey,
+                    }).sort(compileSort(this.state.sortExpr))
+                })} />
+                {view}
+            </div>
+        );
+    }
+}
+
+const QueryView = ({ sensitive, query, focus, allTransactions, transactions, saved, setState, slxConfig, sortExpr, transactionsView }) => (
+    <div>
+        <Query
+            query={query}
+            onChange={query => setState({
+                query,
+            })}
+            onFilter={() => setState({
+                filteredTransactions: select(query, allTransactions, slxConfig),
+            })}
+        />
+        <fieldset>
+            <legend>Sort</legend>
+            <div className="form-group">
+                <input type="text" value={sortExpr} onChange={e => setState({ sortExpr: e.target.value, })} />
+            </div>
+            <div className="form-group">
+                <button className="btn btn-default" onClick={() => setState({
+                    filteredTransactions: transactions.slice().sort(compileSort(sortExpr)),
+                })}>Sort</button>
+            </div>
+        </fieldset>
+        <hr />
+        <ScatterChart transactions={transactions} />
+        <hr />
+        <Transactions
+            transactionsView={transactionsView}
+            sensitive={sensitive}
+            transactions={transactions}
+            onClick={x => setState({
+                saved: { ...saved, [transactionKey(x)]: x.Outflow }
+            })}
+            onViewChange={transactionsView => setState({ transactionsView })}
+        />
+    </div>
+);
+
+function classifyRate(x) {
+    if (x < 0.25) {
+        return 'needs improvement';
+    }
+    if (x < 0.50) {
+        return 'healthy';
+    }
+    if (x < 0.75) {
+        return 'rich';
+    }
+    if (x < 1.00) {
+        return 'wealthy';
+    }
+}
+
+const SavingsView = ({
+    sensitive,
+    categories,
+    savingsView,
+    inflowQuery,
+    outflowQuery,
+    inflowTransactions,
+    outflowTransactions,
+    onSavingsViewChange,
+    onInflowQueryChange,
+    onOutflowQueryChange,
+    onFilterInflow,
+    onFilterOutflow,
+    onFilterSavingsView,
+}) => {
+    const revenue = inflowTransactions.reduce((acc, x) => {
+        acc[x.Date.getMonth()] += x.Inflow;
+        return acc;
+    }, new Array(12).fill(0));
+
+    const inflow = inflowTransactions.reduce((acc, x) => acc + x.Inflow, 0);
+    const outflow = outflowTransactions.reduce((acc, x) => acc + x.Outflow, 0);
+
+    const delta25Sum = new Array(12).fill(0);
+    for (let i = 1; i < 12; i += 1) {
+        delta25Sum[i] = delta25Sum[i - 1] + revenue[i] * 0.25;
+    }
+
+    const delta50Sum = new Array(12).fill(0);
+    for (let i = 1; i < 12; i += 1) {
+        delta50Sum[i] = delta50Sum[i - 1] + revenue[i] * 0.5;
+    }
+
+    const delta75Sum = new Array(12).fill(0);
+    for (let i = 1; i < 12; i += 1) {
+        delta75Sum[i] = delta75Sum[i - 1] + revenue[i] * 0.75;
+    }
+
+    return (
+        <section>
+            <fieldset>
+                <legend>Filtering</legend>
+                <div className="form-group">
+                    <label htmlFor="savings-view">Savings View</label>
+                    <input name="savings-view" type="text" placeholder="Savings View..." value={savingsView}
+                        onChange={e => onSavingsViewChange(e.target.value)} />
+                    <button className="btn btn-default" onClick={() => onFilterSavingsView()}>Apply</button>
+                </div>
+                <div className="form-group">
+                    <label htmlFor="inflow-query">Inflow Query</label>
+                    <input name="inflow-query" type="text" placeholder="Inflow query..." value={inflowQuery}
+                        onChange={e => onInflowQueryChange(e.target.value)} />
+                    <button className="btn btn-default" onClick={() => onFilterInflow()}>Filter</button>
+                </div>
+                <div className="form-group">
+                    <label htmlFor="outflow-query">Outflow Query</label>
+                    <input name="outflow-query" type="text" placeholder="Outflow query..." value={outflowQuery}
+                        onChange={e => onOutflowQueryChange(e.target.value)} />
+                    <button className="btn btn-default" onClick={() => onFilterOutflow()}>Filter</button>
+                </div>
+            </fieldset>
+            <ul>
+                <li>inflow: {dollars(inflow, sensitive)}</li>
+                <li>outflow: {dollars(outflow)}</li>
+                <li>savings: {dollars(inflow - outflow)}</li>
+                <li>rate: {parseFloat((inflow - outflow) / inflow * 100).toFixed(2) + "%"} ({classifyRate((inflow - outflow) / inflow)})</li>
+            </ul>
+            <SavingsRateLineChart rate={(inflow - outflow) / inflow} transactions={outflowTransactions} />
+            {/* O($1,000) */}
+            <StackedHistogram labels={months} datasets={Object.keys(categories).map(k => ({
+                label: k,
+                data: categories[k].reduce((acc, x) => {
+                    acc[x.Date.getMonth()] += x.Outflow;
+                    return acc;
+                }, new Array(12).fill(0)).map((x, i) => ({
+                    x: i,
+                    y: x,
+                }))
+            }))} />
+            {/* O($100) */}
+            <StackedHistogram labels={months} datasets={Object.keys(categories).map(k => ({
+                label: k,
+                data: categories[k].reduce((acc, x) => {
+                    acc[x.Date.getMonth()] += x.Outflow;
+                    return acc;
+                }, new Array(12).fill(0)).map((x, i) => ({
+                    x: i,
+                    y: x,
+                }))
+            }))} />
+            {/* O($10) */}
+            <StackedHistogram labels={months} datasets={Object.keys(categories).map(k => ({
+                label: k,
+                data: categories[k].reduce((acc, x) => {
+                    acc[x.Date.getMonth()] += x.Outflow;
+                    return acc;
+                }, new Array(12).fill(0)).map((x, i) => ({
+                    x: i,
+                    y: x,
+                }))
+            }))} />
+            {/* <GenLineChart x="X" y="Y" datasets={[
+                {
+                    label: '25%',
+                    data: delta25Sum.map((x, i) => ({
+                        x: i,
+                        y: x,
+                    })),
+                },
+                {
+                    label: '50%',
+                    data: delta50Sum.map((x, i) => ({
+                        x: i,
+                        y: x,
+                    })),
+                },
+                {
+                    label: '75%',
+                    data: delta75Sum.map((x, i) => ({
+                        x: i,
+                        y: x,
+                    })),
+                },
+            ]} /> */}
+            <hr />
+            <table>
+                <thead>
+                    <tr>
+                        <th>Month</th>
+                        <th>Delta (25%)</th>
+                        <th>Delta (50%)</th>
+                        <th>Delta (75%)</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    {months.map((x, i) => (
+                        <tr key={i}>
+                            <td>{x}</td>
+                            <td>{dollars(delta25Sum[i], sensitive)}</td>
+                            <td>{dollars(delta50Sum[i], sensitive)}</td>
+                            <td>{dollars(delta75Sum[i], sensitive)}</td>
+                        </tr>
+                    ))}
+                </tbody>
+            </table>
+        </section>
+    );
+};
+
+const Calculator = ({ sensitive, saved, onClear }) => (
+    <div>
+        <ul>
+            {Object.keys(saved).map(k => (
+                <li key={k}>
+                    {dollars(saved[k], sensitive)} {k}
+                </li>
+            ))}
+        </ul>
+        <p>{dollars(savedSum, sensitive)}</p>
+        <button className="btn btn-default" onClick={() => onClear()}>clear</button>
+    </div>
+);
+
+/**
+ * Table rendering information about transactions bucketed by its order of
+ * magnitude.
+ */
+const Magnitable = ({ sensitive, label, transactions }) => {
+    const categories = transactions.reduce((acc, x) => {
+        if (x.Category === '') {
+            return acc;
+        }
+        if (!(x.Category in acc)) {
+            acc[x.Category] = 0;
+        }
+        acc[x.Category] += x.Outflow;
+        return acc;
+    }, {});
+
+    // Sort category keys by sum decreasing.
+    const keys = [...Object.keys(categories)].sort((x, y) => {
+        if (categories[x] < categories[y]) {
+            return 1;
+        } else if (categories[x] > categories[y]) {
+            return -1;
+        } else {
+            return 0;
+        }
+    });
+
+    return (
+        <React.Fragment>
+            {keys.map(k => (
+                <tr style={{ backgroundColor: '#F0F8FF' }}>
+                    <td>{k}</td><td>{dollars(categories[k], sensitive)}</td>
+                </tr>
+            ))}
+        </React.Fragment>
+    );
+};
+
+/**
+ * Calculates and renders various aggregates over an input list of transactions.
+ */
+const AggregateTable = ({ sensitive, focus, onFocus, transactions }) => {
+    const net = transactions.reduce((acc, x) => acc + x.Inflow - x.Outflow, 0);
+    const sum = transactions.reduce((acc, x) => acc + x.Outflow, 0);
+    const buckets = transactions.reduce((acc, x) => {
+        const order = magnitude(x.Outflow);
+        const bucket = Math.pow(10, order);
+        acc[bucket].push(x);
+        return acc;
+    }, { 0.1: [], 0: [], 1: [], 10: [], 100: [], 1000: [] });
+
+    return (
+        <div>
+            <table>
+                <caption>Aggregations</caption>
+                <thead>
+                    <tr>
+                        <th>function</th>
+                        <th>value</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr><td>net</td><td>{dollars(net, sensitive)}</td></tr>
+                    <tr onClick={() => onFocus('sum')}><td>sum</td><td>{dollars(sum, sensitive)}</td></tr>
+                    {focus.sum && <Magnitable sensitive={sensitive} label="sum" transactions={transactions} />}
+                    <tr><td>per day</td><td>{dollars(sum / 365, sensitive)}</td></tr>
+                    <tr><td>per week</td><td>{dollars(sum / 52, sensitive)}</td></tr>
+                    <tr><td>per month</td><td>{dollars(sum / 12, sensitive)}</td></tr>
+                    <tr onClick={() => onFocus(1000)}><td>Σ Θ($1,000)</td><td>{dollars(buckets[1000].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}</td></tr>
+                    {(focus[1000]) && <Magnitable sensitive={sensitive} label="$1,000" transactions={buckets[1000]} />}
+                    <tr onClick={() => onFocus(100)}><td>Σ Θ($100)</td><td>{dollars(buckets[100].reduce((acc, x, sensitive) => acc + x.Outflow, 0), sensitive)}</td></tr>
+                    {(focus[100]) && <Magnitable sensitive={sensitive} label="$100" transactions={buckets[100]} />}
+                    <tr onClick={() => onFocus(10)}><td>Σ Θ($10)</td><td>{dollars(buckets[10].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}</td></tr>
+                    {(focus[10]) && <Magnitable sensitive={sensitive} label="$10" transactions={buckets[10]} />}
+                    <tr onClick={() => onFocus(1)}><td>Σ Θ($1)</td><td>{dollars(buckets[1].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}</td></tr>
+                    {(focus[1]) && <Magnitable sensitive={sensitive} label="$1.00" transactions={buckets[1]} />}
+                    <tr onClick={() => onFocus(0.1)}><td>Σ Θ($0.10)</td><td>{dollars(buckets[0.1].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}</td></tr>
+                    {(focus[0.1]) && <Magnitable sensitive={sensitive} label="$0.10" transactions={buckets[0.1]} />}
+                    <tr><td>average</td><td>{dollars(sum / transactions.length, sensitive)}</td></tr>
+                    <tr><td>count</td><td>{transactions.length}</td></tr>
+                </tbody>
+            </table>
+        </div>
+    );
+};
+
+const Query = ({ query, onChange, onFilter }) => (
+    <fieldset>
+        <legend>Query</legend>
+        <div className="form-group">
+            <input name="query" type="text" value={query} onChange={e => onChange(e.target.value)} />
+        </div>
+        <div className="form-group">
+            <button className="btn btn-default" onClick={() => onFilter()}>Filter</button>
+        </div>
+    </fieldset>
+);
+
+const tableHeaders = [
+    'Account',
+    'Category',
+    'Date',
+    'Inflow',
+    'Outflow',
+    'Payee',
+    'Memo',
+];
+
+const Transactions = ({ sensitive, transactions, onSort, onClick, onViewChange, transactionsView }) => {
+    let view = null;
+    if (transactionsView === 'table') {
+        view = (
+            <table>
+                <thead>
+                    <tr>
+                        {tableHeaders.map(x => <th>{x}</th>)}
+                    </tr>
+                </thead>
+                <tbody>
+                    {transactions.map(x => (
+                        <tr onClick={() => onClick(x)}>
+                            <td>{x.Account}</td>
+                            <td>{x.Category}</td>
+                            <td>{x.Date.toLocaleDateString()}</td>
+                            <td>{dollars(x.Inflow, sensitive)}</td>
+                            <td>{dollars(x.Outflow, sensitive)}</td>
+                            <td>{x.Payee}</td>
+                            <td>{x.Memo}</td>
+                        </tr>
+                    ))}
+                </tbody>
+            </table>
+        );
+    }
+    else if (transactionsView === 'csv') {
+        view = (
+            <code>{tableHeaders.join(',') + '\n' + transactions.map(x => tableHeaders.map(k => x[k]).join(',')).join("\n")}</code>
+        );
+    }
+    else if (transactionsView === 'json') {
+        view = (
+            <code>{JSON.stringify(transactions)}</code>
+        );
+    }
+
+    return (
+        <div>
+            <legend>Transactions</legend>
+            <div className="btn-group">
+                <button onClick={() => onViewChange('table')} className="btn btn-default btn-ghost">Table</button>
+                <button onClick={() => onViewChange('csv')} className="btn btn-default btn-ghost">CSV</button>
+                <button onClick={() => onViewChange('json')} className="btn btn-default btn-ghost">JSON</button>
+            </div>
+            {view}
+        </div>
+    );
+}
+
+const domContainer = document.querySelector('#mount');
+const root = ReactDOM.createRoot(domContainer);
+
+root.render(<App />);
diff --git a/users/wpcarro/ynabsql/dataviz/index.html b/users/wpcarro/ynabsql/dataviz/index.html
new file mode 100644
index 000000000000..e30c8682b25c
--- /dev/null
+++ b/users/wpcarro/ynabsql/dataviz/index.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <!-- TODO(wpcarro): Cache these locally -->
+    <link rel="stylesheet" href="./cdn/terminal.min.css" />
+    <style>
+      :root {
+        --page-width: 100em;
+      }
+    </style>
+  </head>
+  <body class="container">
+    <div id="mount"></div>
+    <!-- depot JS -->
+    <script src="./cdn/slx.js"></script>
+    <script src="./components.jsx" type="module"></script>
+  </body>
+</html>
diff --git a/users/wpcarro/ynabsql/dataviz/package.json b/users/wpcarro/ynabsql/dataviz/package.json
new file mode 100644
index 000000000000..03f795c0bf5a
--- /dev/null
+++ b/users/wpcarro/ynabsql/dataviz/package.json
@@ -0,0 +1,15 @@
+{
+  "devDependencies": {
+    "@parcel/validator-typescript": "^2.8.3",
+    "parcel": "^2.8.3",
+    "process": "^0.11.10",
+    "typescript": ">=3.0.0"
+  },
+  "dependencies": {
+    "chart.js": "^4.2.0",
+    "chartjs-adapter-date-fns": "^3.0.0",
+    "date-fns": "^2.29.3",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
+  }
+}
diff --git a/users/wpcarro/ynabsql/dataviz/yarn.lock b/users/wpcarro/ynabsql/dataviz/yarn.lock
new file mode 100644
index 000000000000..70c52a3a9fb2
--- /dev/null
+++ b/users/wpcarro/ynabsql/dataviz/yarn.lock
@@ -0,0 +1,1540 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@^7.0.0":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
+  integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
+  dependencies:
+    "@babel/highlight" "^7.18.6"
+
+"@babel/helper-validator-identifier@^7.18.6":
+  version "7.19.1"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
+  integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
+
+"@babel/highlight@^7.18.6":
+  version "7.18.6"
+  resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
+  integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.18.6"
+    chalk "^2.0.0"
+    js-tokens "^4.0.0"
+
+"@jridgewell/gen-mapping@^0.3.0":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
+  integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+  dependencies:
+    "@jridgewell/set-array" "^1.0.1"
+    "@jridgewell/sourcemap-codec" "^1.4.10"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@3.1.0":
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+  integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/set-array@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+  integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.2":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
+  integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+  dependencies:
+    "@jridgewell/gen-mapping" "^0.3.0"
+    "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10":
+  version "1.4.14"
+  resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+  integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/trace-mapping@^0.3.9":
+  version "0.3.17"
+  resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
+  integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==
+  dependencies:
+    "@jridgewell/resolve-uri" "3.1.0"
+    "@jridgewell/sourcemap-codec" "1.4.14"
+
+"@kurkle/color@^0.3.0":
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
+  integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
+
+"@lezer/common@^0.15.0", "@lezer/common@^0.15.7":
+  version "0.15.12"
+  resolved "https://registry.yarnpkg.com/@lezer/common/-/common-0.15.12.tgz#2f21aec551dd5fd7d24eb069f90f54d5bc6ee5e9"
+  integrity sha512-edfwCxNLnzq5pBA/yaIhwJ3U3Kz8VAUOTRg0hhxaizaI1N+qxV7EXDv/kLCkLeq2RzSFvxexlaj5Mzfn2kY0Ig==
+
+"@lezer/lr@^0.15.4":
+  version "0.15.8"
+  resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-0.15.8.tgz#1564a911e62b0a0f75ca63794a6aa8c5dc63db21"
+  integrity sha512-bM6oE6VQZ6hIFxDNKk8bKPa14hqFrV07J/vHGOeiAbJReIaQXmkVb6xQu4MR+JBTLa5arGRyAAjJe1qaQt3Uvg==
+  dependencies:
+    "@lezer/common" "^0.15.0"
+
+"@lmdb/lmdb-darwin-arm64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-2.5.2.tgz#bc66fa43286b5c082e8fee0eacc17995806b6fbe"
+  integrity sha512-+F8ioQIUN68B4UFiIBYu0QQvgb9FmlKw2ctQMSBfW2QBrZIxz9vD9jCGqTCPqZBRbPHAS/vG1zSXnKqnS2ch/A==
+
+"@lmdb/lmdb-darwin-x64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-2.5.2.tgz#89d8390041bce6bab24a82a20392be22faf54ffc"
+  integrity sha512-KvPH56KRLLx4KSfKBx0m1r7GGGUMXm0jrKmNE7plbHlesZMuPJICtn07HYgQhj1LNsK7Yqwuvnqh1QxhJnF1EA==
+
+"@lmdb/lmdb-linux-arm64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-2.5.2.tgz#14fe4c96c2bb1285f93797f45915fa35ee047268"
+  integrity sha512-aLl89VHL/wjhievEOlPocoefUyWdvzVrcQ/MHQYZm2JfV1jUsrbr/ZfkPPUFvZBf+VSE+Q0clWs9l29PCX1hTQ==
+
+"@lmdb/lmdb-linux-arm@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-2.5.2.tgz#05bde4573ab10cf21827339fe687148f2590cfa1"
+  integrity sha512-5kQAP21hAkfW5Bl+e0P57dV4dGYnkNIpR7f/GAh6QHlgXx+vp/teVj4PGRZaKAvt0GX6++N6hF8NnGElLDuIDw==
+
+"@lmdb/lmdb-linux-x64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-2.5.2.tgz#d2f85afd857d2c33d2caa5b057944574edafcfee"
+  integrity sha512-xUdUfwDJLGjOUPH3BuPBt0NlIrR7f/QHKgu3GZIXswMMIihAekj2i97oI0iWG5Bok/b+OBjHPfa8IU9velnP/Q==
+
+"@lmdb/lmdb-win32-x64@2.5.2":
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-2.5.2.tgz#28f643fbc0bec30b07fbe95b137879b6b4d1c9c5"
+  integrity sha512-zrBczSbXKxEyK2ijtbRdICDygRqWSRPpZMN5dD1T8VMEW5RIhIbwFWw2phDRXuBQdVDpSjalCIUMWMV2h3JaZA==
+
+"@mischnic/json-sourcemap@^0.1.0":
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/@mischnic/json-sourcemap/-/json-sourcemap-0.1.0.tgz#38af657be4108140a548638267d02a2ea3336507"
+  integrity sha512-dQb3QnfNqmQNYA4nFSN/uLaByIic58gOXq4Y4XqLOWmOrw73KmJPt/HLyG0wvn1bnR6mBKs/Uwvkh+Hns1T0XA==
+  dependencies:
+    "@lezer/common" "^0.15.7"
+    "@lezer/lr" "^0.15.4"
+    json5 "^2.2.1"
+
+"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.2.0.tgz#901c5937e1441572ea23e631fe6deca68482fe76"
+  integrity sha512-Z9LFPzfoJi4mflGWV+rv7o7ZbMU5oAU9VmzCgL240KnqDW65Y2HFCT3MW06/ITJSnbVLacmcEJA8phywK7JinQ==
+
+"@msgpackr-extract/msgpackr-extract-darwin-x64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.2.0.tgz#fb877fe6bae3c4d3cea29786737840e2ae689066"
+  integrity sha512-vq0tT8sjZsy4JdSqmadWVw6f66UXqUCabLmUVHZwUFzMgtgoIIQjT4VVRHKvlof3P/dMCkbMJ5hB1oJ9OWHaaw==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.2.0.tgz#986179c38b10ac41fbdaf7d036c825cbc72855d9"
+  integrity sha512-hlxxLdRmPyq16QCutUtP8Tm6RDWcyaLsRssaHROatgnkOxdleMTgetf9JsdncL8vLh7FVy/RN9i3XR5dnb9cRA==
+
+"@msgpackr-extract/msgpackr-extract-linux-arm@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.2.0.tgz#15f2c6fe9e0adc06c21af7e95f484ff4880d79ce"
+  integrity sha512-SaJ3Qq4lX9Syd2xEo9u3qPxi/OB+5JO/ngJKK97XDpa1C587H9EWYO6KD8995DAjSinWvdHKRrCOXVUC5fvGOg==
+
+"@msgpackr-extract/msgpackr-extract-linux-x64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.2.0.tgz#30cae5c9a202f3e1fa1deb3191b18ffcb2f239a2"
+  integrity sha512-94y5PJrSOqUNcFKmOl7z319FelCLAE0rz/jPCWS+UtdMZvpa4jrQd+cJPQCLp2Fes1yAW/YUQj/Di6YVT3c3Iw==
+
+"@msgpackr-extract/msgpackr-extract-win32-x64@2.2.0":
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.2.0.tgz#016d855b6bc459fd908095811f6826e45dd4ba64"
+  integrity sha512-XrC0JzsqQSvOyM3t04FMLO6z5gCuhPE6k4FXuLK5xf52ZbdvcFe1yBmo7meCew9B8G2f0T9iu9t3kfTYRYROgA==
+
+"@parcel/bundler-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.8.3.tgz#d64739dbc2dbd59d6629861bf77a8083aced5229"
+  integrity sha512-yJvRsNWWu5fVydsWk3O2L4yIy3UZiKWO2cPDukGOIWMgp/Vbpp+2Ct5IygVRtE22bnseW/E/oe0PV3d2IkEJGg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/graph" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/cache@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.8.3.tgz#169e130cf59913c0ed9fadce1a450e68f710e16f"
+  integrity sha512-k7xv5vSQrJLdXuglo+Hv3yF4BCSs1tQ/8Vbd6CHTkOhf7LcGg6CPtLw053R/KdMpd/4GPn0QrAsOLdATm1ELtQ==
+  dependencies:
+    "@parcel/fs" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    lmdb "2.5.2"
+
+"@parcel/codeframe@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.8.3.tgz#84fb529ef70def7f5bc64f6c59b18d24826f5fcc"
+  integrity sha512-FE7sY53D6n/+2Pgg6M9iuEC6F5fvmyBkRE4d9VdnOoxhTXtkEqpqYgX7RJ12FAQwNlxKq4suBJQMgQHMF2Kjeg==
+  dependencies:
+    chalk "^4.1.0"
+
+"@parcel/compressor-raw@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/compressor-raw/-/compressor-raw-2.8.3.tgz#301753df8c6de967553149639e8a4179b88f0c95"
+  integrity sha512-bVDsqleBUxRdKMakWSlWC9ZjOcqDKE60BE+Gh3JSN6WJrycJ02P5wxjTVF4CStNP/G7X17U+nkENxSlMG77ySg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/config-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.8.3.tgz#9a43486e7c702e96c68052c37b79098d7240e35b"
+  integrity sha512-o/A/mbrO6X/BfGS65Sib8d6SSG45NYrNooNBkH/o7zbOBSRQxwyTlysleK1/3Wa35YpvFyLOwgfakqCtbGy4fw==
+  dependencies:
+    "@parcel/bundler-default" "2.8.3"
+    "@parcel/compressor-raw" "2.8.3"
+    "@parcel/namer-default" "2.8.3"
+    "@parcel/optimizer-css" "2.8.3"
+    "@parcel/optimizer-htmlnano" "2.8.3"
+    "@parcel/optimizer-image" "2.8.3"
+    "@parcel/optimizer-svgo" "2.8.3"
+    "@parcel/optimizer-terser" "2.8.3"
+    "@parcel/packager-css" "2.8.3"
+    "@parcel/packager-html" "2.8.3"
+    "@parcel/packager-js" "2.8.3"
+    "@parcel/packager-raw" "2.8.3"
+    "@parcel/packager-svg" "2.8.3"
+    "@parcel/reporter-dev-server" "2.8.3"
+    "@parcel/resolver-default" "2.8.3"
+    "@parcel/runtime-browser-hmr" "2.8.3"
+    "@parcel/runtime-js" "2.8.3"
+    "@parcel/runtime-react-refresh" "2.8.3"
+    "@parcel/runtime-service-worker" "2.8.3"
+    "@parcel/transformer-babel" "2.8.3"
+    "@parcel/transformer-css" "2.8.3"
+    "@parcel/transformer-html" "2.8.3"
+    "@parcel/transformer-image" "2.8.3"
+    "@parcel/transformer-js" "2.8.3"
+    "@parcel/transformer-json" "2.8.3"
+    "@parcel/transformer-postcss" "2.8.3"
+    "@parcel/transformer-posthtml" "2.8.3"
+    "@parcel/transformer-raw" "2.8.3"
+    "@parcel/transformer-react-refresh-wrap" "2.8.3"
+    "@parcel/transformer-svg" "2.8.3"
+
+"@parcel/core@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.8.3.tgz#22a69f36095d53736ab10bf42697d9aa5f4e382b"
+  integrity sha512-Euf/un4ZAiClnlUXqPB9phQlKbveU+2CotZv7m7i+qkgvFn5nAGnrV4h1OzQU42j9dpgOxWi7AttUDMrvkbhCQ==
+  dependencies:
+    "@mischnic/json-sourcemap" "^0.1.0"
+    "@parcel/cache" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/events" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/graph" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/package-manager" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    abortcontroller-polyfill "^1.1.9"
+    base-x "^3.0.8"
+    browserslist "^4.6.6"
+    clone "^2.1.1"
+    dotenv "^7.0.0"
+    dotenv-expand "^5.1.0"
+    json5 "^2.2.0"
+    msgpackr "^1.5.4"
+    nullthrows "^1.1.1"
+    semver "^5.7.1"
+
+"@parcel/diagnostic@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.8.3.tgz#d560276d5d2804b48beafa1feaf3fc6b2ac5e39d"
+  integrity sha512-u7wSzuMhLGWZjVNYJZq/SOViS3uFG0xwIcqXw12w54Uozd6BH8JlhVtVyAsq9kqnn7YFkw6pXHqAo5Tzh4FqsQ==
+  dependencies:
+    "@mischnic/json-sourcemap" "^0.1.0"
+    nullthrows "^1.1.1"
+
+"@parcel/events@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.8.3.tgz#205f8d874e6ecc2cbdb941bf8d54bae669e571af"
+  integrity sha512-hoIS4tAxWp8FJk3628bsgKxEvR7bq2scCVYHSqZ4fTi/s0+VymEATrRCUqf+12e5H47uw1/ZjoqrGtBI02pz4w==
+
+"@parcel/fs-search@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/fs-search/-/fs-search-2.8.3.tgz#1c7d812c110b808758f44c56e61dfffdb09e9451"
+  integrity sha512-DJBT2N8knfN7Na6PP2mett3spQLTqxFrvl0gv+TJRp61T8Ljc4VuUTb0hqBj+belaASIp3Q+e8+SgaFQu7wLiQ==
+  dependencies:
+    detect-libc "^1.0.3"
+
+"@parcel/fs@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.8.3.tgz#80536afe877fc8a2bd26be5576b9ba27bb4c5754"
+  integrity sha512-y+i+oXbT7lP0e0pJZi/YSm1vg0LDsbycFuHZIL80pNwdEppUAtibfJZCp606B7HOjMAlNZOBo48e3hPG3d8jgQ==
+  dependencies:
+    "@parcel/fs-search" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/watcher" "^2.0.7"
+    "@parcel/workers" "2.8.3"
+
+"@parcel/graph@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/graph/-/graph-2.8.3.tgz#00ffe8ec032e74fee57199e54529f1da7322571d"
+  integrity sha512-26GL8fYZPdsRhSXCZ0ZWliloK6DHlMJPWh6Z+3VVZ5mnDSbYg/rRKWmrkhnr99ZWmL9rJsv4G74ZwvDEXTMPBg==
+  dependencies:
+    nullthrows "^1.1.1"
+
+"@parcel/hash@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/hash/-/hash-2.8.3.tgz#bc2499a27395169616cad2a99e19e69b9098f6e9"
+  integrity sha512-FVItqzjWmnyP4ZsVgX+G00+6U2IzOvqDtdwQIWisCcVoXJFCqZJDy6oa2qDDFz96xCCCynjRjPdQx2jYBCpfYw==
+  dependencies:
+    detect-libc "^1.0.3"
+    xxhash-wasm "^0.4.2"
+
+"@parcel/logger@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.8.3.tgz#e14e4debafb3ca9e87c07c06780f9afc38b2712c"
+  integrity sha512-Kpxd3O/Vs7nYJIzkdmB6Bvp3l/85ydIxaZaPfGSGTYOfaffSOTkhcW9l6WemsxUrlts4za6CaEWcc4DOvaMOPA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/events" "2.8.3"
+
+"@parcel/markdown-ansi@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.8.3.tgz#1337d421bb1133ad178f386a8e1b746631bba4a1"
+  integrity sha512-4v+pjyoh9f5zuU/gJlNvNFGEAb6J90sOBwpKJYJhdWXLZMNFCVzSigxrYO+vCsi8G4rl6/B2c0LcwIMjGPHmFQ==
+  dependencies:
+    chalk "^4.1.0"
+
+"@parcel/namer-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.8.3.tgz#5304bee74beb4b9c1880781bdbe35be0656372f4"
+  integrity sha512-tJ7JehZviS5QwnxbARd8Uh63rkikZdZs1QOyivUhEvhN+DddSAVEdQLHGPzkl3YRk0tjFhbqo+Jci7TpezuAMw==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/node-resolver-core@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-2.8.3.tgz#581df074a27646400b3fed9da95297b616a7db8f"
+  integrity sha512-12YryWcA5Iw2WNoEVr/t2HDjYR1iEzbjEcxfh1vaVDdZ020PiGw67g5hyIE/tsnG7SRJ0xdRx1fQ2hDgED+0Ww==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    semver "^5.7.1"
+
+"@parcel/optimizer-css@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-css/-/optimizer-css-2.8.3.tgz#420a333f4b78f7ff15e69217dfed34421b1143ee"
+  integrity sha512-JotGAWo8JhuXsQDK0UkzeQB0UR5hDAKvAviXrjqB4KM9wZNLhLleeEAW4Hk8R9smCeQFP6Xg/N/NkLDpqMwT3g==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    browserslist "^4.6.6"
+    lightningcss "^1.16.1"
+    nullthrows "^1.1.1"
+
+"@parcel/optimizer-htmlnano@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.8.3.tgz#a71ab6f0f24160ef9f573266064438eff65e96d0"
+  integrity sha512-L8/fHbEy8Id2a2E0fwR5eKGlv9VYDjrH9PwdJE9Za9v1O/vEsfl/0T/79/x129l5O0yB6EFQkFa20MiK3b+vOg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    htmlnano "^2.0.0"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    svgo "^2.4.0"
+
+"@parcel/optimizer-image@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-image/-/optimizer-image-2.8.3.tgz#ea49b4245b4f7d60b38c7585c6311fb21d341baa"
+  integrity sha512-SD71sSH27SkCDNUNx9A3jizqB/WIJr3dsfp+JZGZC42tpD/Siim6Rqy9M4To/BpMMQIIiEXa5ofwS+DgTEiEHQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    detect-libc "^1.0.3"
+
+"@parcel/optimizer-svgo@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-svgo/-/optimizer-svgo-2.8.3.tgz#04da4efec6b623679539a84961bff6998034ba8a"
+  integrity sha512-9KQed99NZnQw3/W4qBYVQ7212rzA9EqrQG019TIWJzkA9tjGBMIm2c/nXpK1tc3hQ3e7KkXkFCQ3C+ibVUnHNA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    svgo "^2.4.0"
+
+"@parcel/optimizer-terser@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/optimizer-terser/-/optimizer-terser-2.8.3.tgz#3a06d98d09386a1a0ae1be85376a8739bfba9618"
+  integrity sha512-9EeQlN6zIeUWwzrzu6Q2pQSaYsYGah8MtiQ/hog9KEPlYTP60hBv/+utDyYEHSQhL7y5ym08tPX5GzBvwAD/dA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    terser "^5.2.0"
+
+"@parcel/package-manager@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.8.3.tgz#ddd0d62feae3cf0fb6cc0537791b3a16296ad458"
+  integrity sha512-tIpY5pD2lH53p9hpi++GsODy6V3khSTX4pLEGuMpeSYbHthnOViobqIlFLsjni+QA1pfc8NNNIQwSNdGjYflVA==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    semver "^5.7.1"
+
+"@parcel/packager-css@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.8.3.tgz#0eff34268cb4f5dfb53c1bbca85f5567aeb1835a"
+  integrity sha512-WyvkMmsurlHG8d8oUVm7S+D+cC/T3qGeqogb7sTI52gB6uiywU7lRCizLNqGFyFGIxcVTVHWnSHqItBcLN76lA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/packager-html@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.8.3.tgz#f9263b891aa4dd46c6e2fa2b07025a482132fff1"
+  integrity sha512-OhPu1Hx1RRKJodpiu86ZqL8el2Aa4uhBHF6RAL1Pcrh2EhRRlPf70Sk0tC22zUpYL7es+iNKZ/n0Rl+OWSHWEw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+
+"@parcel/packager-js@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.8.3.tgz#3ed11565915d73d12192b6901c75a6b820e4a83a"
+  integrity sha512-0pGKC3Ax5vFuxuZCRB+nBucRfFRz4ioie19BbDxYnvBxrd4M3FIu45njf6zbBYsI9eXqaDnL1b3DcZJfYqtIzw==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    globals "^13.2.0"
+    nullthrows "^1.1.1"
+
+"@parcel/packager-raw@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.8.3.tgz#bdec826df991e186cb58691cc45d12ad5c06676e"
+  integrity sha512-BA6enNQo1RCnco9MhkxGrjOk59O71IZ9DPKu3lCtqqYEVd823tXff2clDKHK25i6cChmeHu6oB1Rb73hlPqhUA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/packager-svg@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/packager-svg/-/packager-svg-2.8.3.tgz#7233315296001c531cb55ca96b5f2ef672343630"
+  integrity sha512-mvIoHpmv5yzl36OjrklTDFShLUfPFTwrmp1eIwiszGdEBuQaX7JVI3Oo2jbVQgcN4W7J6SENzGQ3Q5hPTW3pMw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    posthtml "^0.16.4"
+
+"@parcel/plugin@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.8.3.tgz#7bb30a5775eaa6473c27f002a0a3ee7308d6d669"
+  integrity sha512-jZ6mnsS4D9X9GaNnvrixDQwlUQJCohDX2hGyM0U0bY2NWU8Km97SjtoCpWjq+XBCx/gpC4g58+fk9VQeZq2vlw==
+  dependencies:
+    "@parcel/types" "2.8.3"
+
+"@parcel/reporter-cli@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.8.3.tgz#12a4743b51b8fe6837f53c20e01bbf1f7336e8e4"
+  integrity sha512-3sJkS6tFFzgIOz3u3IpD/RsmRxvOKKiQHOTkiiqRt1l44mMDGKS7zANRnJYsQzdCsgwc9SOP30XFgJwtoVlMbw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    chalk "^4.1.0"
+    term-size "^2.2.1"
+
+"@parcel/reporter-dev-server@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.8.3.tgz#a0daa5cc015642684cea561f4e0e7116bbffdc1c"
+  integrity sha512-Y8C8hzgzTd13IoWTj+COYXEyCkXfmVJs3//GDBsH22pbtSFMuzAZd+8J9qsCo0EWpiDow7V9f1LischvEh3FbQ==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+
+"@parcel/resolver-default@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.8.3.tgz#5ae41e537ae4a793c1abb47f094482b9e2ac3535"
+  integrity sha512-k0B5M/PJ+3rFbNj4xZSBr6d6HVIe6DH/P3dClLcgBYSXAvElNDfXgtIimbjCyItFkW9/BfcgOVKEEIZOeySH/A==
+  dependencies:
+    "@parcel/node-resolver-core" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/runtime-browser-hmr@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.8.3.tgz#1fa74e1fbd1030b0a920c58afa3a9eb7dc4bcd1e"
+  integrity sha512-2O1PYi2j/Q0lTyGNV3JdBYwg4rKo6TEVFlYGdd5wCYU9ZIN9RRuoCnWWH2qCPj3pjIVtBeppYxzfVjPEHINWVg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+
+"@parcel/runtime-js@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.8.3.tgz#0baa4c8fbf77eabce05d01ccc186614968ffc0cd"
+  integrity sha512-IRja0vNKwvMtPgIqkBQh0QtRn0XcxNC8HU1jrgWGRckzu10qJWO+5ULgtOeR4pv9krffmMPqywGXw6l/gvJKYQ==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/runtime-react-refresh@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.8.3.tgz#381a942fb81e8f5ac6c7e0ee1b91dbf34763c3f8"
+  integrity sha512-2v/qFKp00MfG0234OdOgQNAo6TLENpFYZMbVbAsPMY9ITiqG73MrEsrGXVoGbYiGTMB/Toer/lSWlJxtacOCuA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    react-error-overlay "6.0.9"
+    react-refresh "^0.9.0"
+
+"@parcel/runtime-service-worker@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/runtime-service-worker/-/runtime-service-worker-2.8.3.tgz#54d92da9ff1dfbd27db0e84164a22fa59e99b348"
+  integrity sha512-/Skkw+EeRiwzOJso5fQtK8c9b452uWLNhQH1ISTodbmlcyB4YalAiSsyHCtMYD0c3/t5Sx4ZS7vxBAtQd0RvOw==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/source-map@^2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@parcel/source-map/-/source-map-2.1.1.tgz#fb193b82dba6dd62cc7a76b326f57bb35000a782"
+  integrity sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==
+  dependencies:
+    detect-libc "^1.0.3"
+
+"@parcel/transformer-babel@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.8.3.tgz#286bc6cb9afe4c0259f0b28e0f2f47322a24b130"
+  integrity sha512-L6lExfpvvC7T/g3pxf3CIJRouQl+sgrSzuWQ0fD4PemUDHvHchSP4SNUVnd6gOytF3Y1KpnEZIunQGi5xVqQCQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    browserslist "^4.6.6"
+    json5 "^2.2.0"
+    nullthrows "^1.1.1"
+    semver "^5.7.0"
+
+"@parcel/transformer-css@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.8.3.tgz#d6c44100204e73841ad8e0f90472172ea8b9120c"
+  integrity sha512-xTqFwlSXtnaYen9ivAgz+xPW7yRl/u4QxtnDyDpz5dr8gSeOpQYRcjkd4RsYzKsWzZcGtB5EofEk8ayUbWKEUg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    browserslist "^4.6.6"
+    lightningcss "^1.16.1"
+    nullthrows "^1.1.1"
+
+"@parcel/transformer-html@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.8.3.tgz#5c68b28ee6b8c7a13b8aee87f7957ad3227bd83f"
+  integrity sha512-kIZO3qsMYTbSnSpl9cnZog+SwL517ffWH54JeB410OSAYF1ouf4n5v9qBnALZbuCCmPwJRGs4jUtE452hxwN4g==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.10.1"
+    posthtml-render "^3.0.0"
+    semver "^5.7.1"
+    srcset "4"
+
+"@parcel/transformer-image@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.8.3.tgz#73805b2bfc3c8919d7737544e5f8be39e3f303fe"
+  integrity sha512-cO4uptcCGTi5H6bvTrAWEFUsTNhA4kCo8BSvRSCHA2sf/4C5tGQPHt3JhdO0GQLPwZRCh/R41EkJs5HZ8A8DAg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    nullthrows "^1.1.1"
+
+"@parcel/transformer-js@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.8.3.tgz#fe400df428394d1e7fe5afb6dea5c7c858e44f03"
+  integrity sha512-9Qd6bib+sWRcpovvzvxwy/PdFrLUXGfmSW9XcVVG8pvgXsZPFaNjnNT8stzGQj1pQiougCoxMY4aTM5p1lGHEQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/utils" "2.8.3"
+    "@parcel/workers" "2.8.3"
+    "@swc/helpers" "^0.4.12"
+    browserslist "^4.6.6"
+    detect-libc "^1.0.3"
+    nullthrows "^1.1.1"
+    regenerator-runtime "^0.13.7"
+    semver "^5.7.1"
+
+"@parcel/transformer-json@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.8.3.tgz#25deb3a5138cc70a83269fc5d39d564609354d36"
+  integrity sha512-B7LmVq5Q7bZO4ERb6NHtRuUKWGysEeaj9H4zelnyBv+wLgpo4f5FCxSE1/rTNmP9u1qHvQ3scGdK6EdSSokGPg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    json5 "^2.2.0"
+
+"@parcel/transformer-postcss@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.8.3.tgz#df4fdc1c90893823445f2a8eb8e2bdd0349ccc58"
+  integrity sha512-e8luB/poIlz6jBsD1Izms+6ElbyzuoFVa4lFVLZnTAChI3UxPdt9p/uTsIO46HyBps/Bk8ocvt3J4YF84jzmvg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    clone "^2.1.1"
+    nullthrows "^1.1.1"
+    postcss-value-parser "^4.2.0"
+    semver "^5.7.1"
+
+"@parcel/transformer-posthtml@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.8.3.tgz#7c3912a5a631cb26485f6464e0d6eeabb6f1e718"
+  integrity sha512-pkzf9Smyeaw4uaRLsT41RGrPLT5Aip8ZPcntawAfIo+KivBQUV0erY1IvHYjyfFzq1ld/Fo2Ith9He6mxpPifA==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.10.1"
+    posthtml-render "^3.0.0"
+    semver "^5.7.1"
+
+"@parcel/transformer-raw@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.8.3.tgz#3a22213fe18a5f83fd78889cb49f06e059cfead7"
+  integrity sha512-G+5cXnd2/1O3nV/pgRxVKZY/HcGSseuhAe71gQdSQftb8uJEURyUHoQ9Eh0JUD3MgWh9V+nIKoyFEZdf9T0sUQ==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+
+"@parcel/transformer-react-refresh-wrap@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.8.3.tgz#8b0392638405dd470a886002229f7889d5464822"
+  integrity sha512-q8AAoEvBnCf/nPvgOwFwKZfEl/thwq7c2duxXkhl+tTLDRN2vGmyz4355IxCkavSX+pLWSQ5MexklSEeMkgthg==
+  dependencies:
+    "@parcel/plugin" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    react-refresh "^0.9.0"
+
+"@parcel/transformer-svg@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/transformer-svg/-/transformer-svg-2.8.3.tgz#4df959cba4ebf45d7aaddd540f752e6e84df38b2"
+  integrity sha512-3Zr/gBzxi1ZH1fftH/+KsZU7w5GqkmxlB0ZM8ovS5E/Pl1lq1t0xvGJue9m2VuQqP8Mxfpl5qLFmsKlhaZdMIQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    nullthrows "^1.1.1"
+    posthtml "^0.16.5"
+    posthtml-parser "^0.10.1"
+    posthtml-render "^3.0.0"
+    semver "^5.7.1"
+
+"@parcel/ts-utils@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/ts-utils/-/ts-utils-2.8.3.tgz#f3590ca033c061779dc35ff3d14af2860ed106ac"
+  integrity sha512-4HMt9B9LF2pDFvSKGImho48tlCvCUl7ly1ZMXvQdmEq2i0yoS81tDsmxX3yly/RVUVeUCGAj1JRuuy1lw5zw1A==
+  dependencies:
+    nullthrows "^1.1.1"
+
+"@parcel/types@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.8.3.tgz#3306bc5391b6913bd619914894b8cd84a24b30fa"
+  integrity sha512-FECA1FB7+0UpITKU0D6TgGBpGxYpVSMNEENZbSJxFSajNy3wrko+zwBKQmFOLOiPcEtnGikxNs+jkFWbPlUAtw==
+  dependencies:
+    "@parcel/cache" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/package-manager" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    "@parcel/workers" "2.8.3"
+    utility-types "^3.10.0"
+
+"@parcel/utils@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.8.3.tgz#0d56c9e8e22c119590a5e044a0e01031965da40e"
+  integrity sha512-IhVrmNiJ+LOKHcCivG5dnuLGjhPYxQ/IzbnF2DKNQXWBTsYlHkJZpmz7THoeLtLliGmSOZ3ZCsbR8/tJJKmxjA==
+  dependencies:
+    "@parcel/codeframe" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/hash" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/markdown-ansi" "2.8.3"
+    "@parcel/source-map" "^2.1.1"
+    chalk "^4.1.0"
+
+"@parcel/validator-typescript@^2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/validator-typescript/-/validator-typescript-2.8.3.tgz#6f9cb5b48df302b1d65e9b17dc1a20870e746976"
+  integrity sha512-2UYGCAwrxh7HIGcrXl8Vu9Sisd8vAu/6Jp/oJV5n9ZQuT5O9pQAlK2lZGSocYRucBtmb4WajII2S2GTzUZeEuQ==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/plugin" "2.8.3"
+    "@parcel/ts-utils" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+
+"@parcel/watcher@^2.0.7":
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.1.0.tgz#5f32969362db4893922c526a842d8af7a8538545"
+  integrity sha512-8s8yYjd19pDSsBpbkOHnT6Z2+UJSuLQx61pCFM0s5wSRvKCEMDjd/cHY3/GI1szHIWbpXpsJdg3V6ISGGx9xDw==
+  dependencies:
+    is-glob "^4.0.3"
+    micromatch "^4.0.5"
+    node-addon-api "^3.2.1"
+    node-gyp-build "^4.3.0"
+
+"@parcel/workers@2.8.3":
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.8.3.tgz#255450ccf4db234082407e4ddda5fd575f08c235"
+  integrity sha512-+AxBnKgjqVpUHBcHLWIHcjYgKIvHIpZjN33mG5LG9XXvrZiqdWvouEzqEXlVLq5VzzVbKIQQcmsvRy138YErkg==
+  dependencies:
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/types" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    chrome-trace-event "^1.0.2"
+    nullthrows "^1.1.1"
+
+"@swc/helpers@^0.4.12":
+  version "0.4.14"
+  resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74"
+  integrity sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==
+  dependencies:
+    tslib "^2.4.0"
+
+"@trysound/sax@0.2.0":
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
+  integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
+
+"@types/parse-json@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
+  integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
+
+abortcontroller-polyfill@^1.1.9:
+  version "1.7.5"
+  resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz#6738495f4e901fbb57b6c0611d0c75f76c485bed"
+  integrity sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==
+
+acorn@^8.5.0:
+  version "8.8.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73"
+  integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==
+
+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.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+base-x@^3.0.8:
+  version "3.0.9"
+  resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
+  integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
+  dependencies:
+    safe-buffer "^5.0.1"
+
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
+braces@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browserslist@^4.6.6:
+  version "4.21.4"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
+  integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==
+  dependencies:
+    caniuse-lite "^1.0.30001400"
+    electron-to-chromium "^1.4.251"
+    node-releases "^2.0.6"
+    update-browserslist-db "^1.0.9"
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+callsites@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
+  integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+caniuse-lite@^1.0.30001400:
+  version "1.0.30001446"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001446.tgz#6d4ba828ab19f49f9bcd14a8430d30feebf1e0c5"
+  integrity sha512-fEoga4PrImGcwUUGEol/PoFCSBnSkA9drgdkxXkJLsUBOnJ8rs3zDv6ApqYXGQFOyMPsjh79naWhF4DAxbF8rw==
+
+chalk@^2.0.0:
+  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@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+chart.js@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.2.0.tgz#dd281b2ce890bff32f3e249cf2972a1e74bc032c"
+  integrity sha512-wbtcV+QKeH0F7gQZaCJEIpsNriFheacouJQTVIjITi3eQA8bTlIBoknz0+dgV79aeKLNMAX+nDslIVE/nJ3rzA==
+  dependencies:
+    "@kurkle/color" "^0.3.0"
+
+chartjs-adapter-date-fns@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz#c25f63c7f317c1f96f9a7c44bd45eeedb8a478e5"
+  integrity sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==
+
+chrome-trace-event@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+  integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+clone@^2.1.1:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+  integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
+
+color-convert@^1.9.0:
+  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 sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
+
+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==
+
+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==
+
+commander@^7.0.0, commander@^7.2.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+cosmiconfig@^7.0.1:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6"
+  integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==
+  dependencies:
+    "@types/parse-json" "^4.0.0"
+    import-fresh "^3.2.1"
+    parse-json "^5.0.0"
+    path-type "^4.0.0"
+    yaml "^1.10.0"
+
+css-select@^4.1.3:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
+  integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^6.0.1"
+    domhandler "^4.3.1"
+    domutils "^2.8.0"
+    nth-check "^2.0.1"
+
+css-tree@^1.1.2, css-tree@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+  dependencies:
+    mdn-data "2.0.14"
+    source-map "^0.6.1"
+
+css-what@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
+  integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
+
+csso@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+  dependencies:
+    css-tree "^1.1.2"
+
+date-fns@^2.29.3:
+  version "2.29.3"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
+  integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
+
+detect-libc@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+  integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
+
+dom-serializer@^1.0.1:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
+  integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.0"
+    entities "^2.0.0"
+
+domelementtype@^2.0.1, domelementtype@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
+domhandler@^4.2.0, domhandler@^4.2.2, domhandler@^4.3.1:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+  integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@^2.8.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.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@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c"
+  integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==
+
+electron-to-chromium@^1.4.251:
+  version "1.4.284"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
+  integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==
+
+entities@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
+entities@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4"
+  integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==
+
+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"
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+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 sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+get-port@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119"
+  integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==
+
+globals@^13.2.0:
+  version "13.19.0"
+  resolved "https://registry.yarnpkg.com/globals/-/globals-13.19.0.tgz#7a42de8e6ad4f7242fbcca27ea5b23aca367b5c8"
+  integrity sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==
+  dependencies:
+    type-fest "^0.20.2"
+
+has-flag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
+  integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==
+
+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==
+
+htmlnano@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-2.0.3.tgz#50ee639ed63357d4a6c01309f52a35892e4edc2e"
+  integrity sha512-S4PGGj9RbdgW8LhbILNK7W9JhmYP8zmDY7KDV/8eCiJBQJlbmltp5I0gv8c5ntLljfdxxfmJ+UJVSqyH4mb41A==
+  dependencies:
+    cosmiconfig "^7.0.1"
+    posthtml "^0.16.5"
+    timsort "^0.3.0"
+
+htmlparser2@^7.1.1:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5"
+  integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.2"
+    domutils "^2.8.0"
+    entities "^3.0.1"
+
+import-fresh@^3.2.1:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
+  integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
+  dependencies:
+    parent-module "^1.0.0"
+    resolve-from "^4.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 sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-glob@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-json@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-json/-/is-json-2.0.1.tgz#6be166d144828a131d686891b983df62c39491ff"
+  integrity sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+"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==
+
+json-parse-even-better-errors@^2.3.0:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
+  integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
+
+json5@^2.2.0, json5@^2.2.1:
+  version "2.2.3"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283"
+  integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+lightningcss-darwin-arm64@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.18.0.tgz#bcd7d494d99c69947abd71136a42e80dfa80c682"
+  integrity sha512-OqjydwtiNPgdH1ByIjA1YzqvDG/OMR6L3LPN6wRl1729LB0y4Mik7L06kmZaTb+pvUHr+NmDd2KCwnlrQ4zO3w==
+
+lightningcss-darwin-x64@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.18.0.tgz#952abea2405fe2bb8dd0bb57a9d5590f8d1d6414"
+  integrity sha512-mNiuPHj89/JHZmJMp+5H8EZSt6EL5DZRWJ31O6k3DrLLnRIQjXuXdDdN8kP7LoIkeWI5xvyD60CsReJm+YWYAw==
+
+lightningcss-linux-arm-gnueabihf@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.18.0.tgz#23ca85e05dc4def9b4975aef307554ef292b56cd"
+  integrity sha512-S+25JjI6601HiAVoTDXW6SqH+E94a+FHA7WQqseyNHunOgVWKcAkNEc2LJvVxgwTq6z41sDIb9/M3Z9wa9lk4A==
+
+lightningcss-linux-arm64-gnu@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.18.0.tgz#6c8e0a6e2c8b44cf180f3a0f0740402e8f656155"
+  integrity sha512-JSqh4+21dCgBecIQUet35dtE4PhhSEMyqe3y0ZNQrAJQ5kyUPSQHiw81WXnPJcOSTTpG0TyMLiC8K//+BsFGQA==
+
+lightningcss-linux-arm64-musl@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.18.0.tgz#88393c101cf236ea0cdc97fddd66b82db964d835"
+  integrity sha512-2FWHa8iUhShnZnqhn2wfIcK5adJat9hAAaX7etNsoXJymlliDIOFuBQEsba2KBAZSM4QqfQtvRdR7m8i0I7ybQ==
+
+lightningcss-linux-x64-gnu@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.18.0.tgz#ad068d24836568337bfe545650565e13f813c8ee"
+  integrity sha512-plCPGQJtDZHcLVKVRLnQVF2XRsIC32WvuJhQ7fJ7F6BV98b/VZX0OlX05qUaOESD9dCDHjYSfxsgcvOKgCWh7A==
+
+lightningcss-linux-x64-musl@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.18.0.tgz#4d84de26b8185aa42450e0f4c83bbfb5a36ae750"
+  integrity sha512-na+BGtVU6fpZvOHKhnlA0XHeibkT3/46nj6vLluG3kzdJYoBKU6dIl7DSOk++8jv4ybZyFJ0aOFMMSc8g2h58A==
+
+lightningcss-win32-x64-msvc@1.18.0:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.18.0.tgz#f83952d16b83dfce65f4615f87c867769220d117"
+  integrity sha512-5qeAH4RMNy2yMNEl7e5TI6upt/7xD2ZpHWH4RkT8iJ7/6POS5mjHbXWUO9Q1hhDhqkdzGa76uAdMzEouIeCyNw==
+
+lightningcss@^1.16.1:
+  version "1.18.0"
+  resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.18.0.tgz#ca3327a1a7571a83bbb9733ed4e4cded775bdadf"
+  integrity sha512-uk10tNxi5fhZqU93vtYiQgx/8a9f0Kvtj5AXIm+VlOXY+t/DWDmCZWJEkZJmmALgvbS6aAW8or+Kq85eJ6TDTw==
+  dependencies:
+    detect-libc "^1.0.3"
+  optionalDependencies:
+    lightningcss-darwin-arm64 "1.18.0"
+    lightningcss-darwin-x64 "1.18.0"
+    lightningcss-linux-arm-gnueabihf "1.18.0"
+    lightningcss-linux-arm64-gnu "1.18.0"
+    lightningcss-linux-arm64-musl "1.18.0"
+    lightningcss-linux-x64-gnu "1.18.0"
+    lightningcss-linux-x64-musl "1.18.0"
+    lightningcss-win32-x64-msvc "1.18.0"
+
+lines-and-columns@^1.1.6:
+  version "1.2.4"
+  resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
+  integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+lmdb@2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/lmdb/-/lmdb-2.5.2.tgz#37e28a9fb43405f4dc48c44cec0e13a14c4a6ff1"
+  integrity sha512-V5V5Xa2Hp9i2XsbDALkBTeHXnBXh/lEmk9p22zdr7jtuOIY9TGhjK6vAvTpOOx9IKU4hJkRWZxn/HsvR1ELLtA==
+  dependencies:
+    msgpackr "^1.5.4"
+    node-addon-api "^4.3.0"
+    node-gyp-build-optional-packages "5.0.3"
+    ordered-binary "^1.2.4"
+    weak-lru-cache "^1.2.2"
+  optionalDependencies:
+    "@lmdb/lmdb-darwin-arm64" "2.5.2"
+    "@lmdb/lmdb-darwin-x64" "2.5.2"
+    "@lmdb/lmdb-linux-arm" "2.5.2"
+    "@lmdb/lmdb-linux-arm64" "2.5.2"
+    "@lmdb/lmdb-linux-x64" "2.5.2"
+    "@lmdb/lmdb-win32-x64" "2.5.2"
+
+loose-envify@^1.1.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"
+
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
+micromatch@^4.0.5:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
+  integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
+  dependencies:
+    braces "^3.0.2"
+    picomatch "^2.3.1"
+
+msgpackr-extract@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.2.0.tgz#4bb749b58d9764cfdc0d91c7977a007b08e8f262"
+  integrity sha512-0YcvWSv7ZOGl9Od6Y5iJ3XnPww8O7WLcpYMDwX+PAA/uXLDtyw94PJv9GLQV/nnp3cWlDhMoyKZIQLrx33sWog==
+  dependencies:
+    node-gyp-build-optional-packages "5.0.3"
+  optionalDependencies:
+    "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-linux-arm" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-linux-x64" "2.2.0"
+    "@msgpackr-extract/msgpackr-extract-win32-x64" "2.2.0"
+
+msgpackr@^1.5.4:
+  version "1.8.1"
+  resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.8.1.tgz#2298aed8a14f83e99df77d344cbda3e436f29b5b"
+  integrity sha512-05fT4J8ZqjYlR4QcRDIhLCYKUOHXk7C/xa62GzMKj74l3up9k2QZ3LgFc6qWdsPHl91QA2WLWqWc8b8t7GLNNw==
+  optionalDependencies:
+    msgpackr-extract "^2.2.0"
+
+node-addon-api@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
+  integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
+
+node-addon-api@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
+  integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
+
+node-gyp-build-optional-packages@5.0.3:
+  version "5.0.3"
+  resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17"
+  integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA==
+
+node-gyp-build@^4.3.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055"
+  integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==
+
+node-releases@^2.0.6:
+  version "2.0.8"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.8.tgz#0f349cdc8fcfa39a92ac0be9bc48b7706292b9ae"
+  integrity sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==
+
+nth-check@^2.0.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
+  integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
+  dependencies:
+    boolbase "^1.0.0"
+
+nullthrows@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1"
+  integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==
+
+ordered-binary@^1.2.4:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/ordered-binary/-/ordered-binary-1.4.0.tgz#6bb53d44925f3b8afc33d1eed0fa15693b211389"
+  integrity sha512-EHQ/jk4/a9hLupIKxTfUsQRej1Yd/0QLQs3vGvIqg5ZtCYSzNhkzHoZc7Zf4e4kUlDaC3Uw8Q/1opOLNN2OKRQ==
+
+parcel@^2.8.3:
+  version "2.8.3"
+  resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.8.3.tgz#1ff71d7317274fd367379bc7310a52c6b75d30c2"
+  integrity sha512-5rMBpbNE72g6jZvkdR5gS2nyhwIXaJy8i65osOqs/+5b7zgf3eMKgjSsDrv6bhz3gzifsba6MBJiZdBckl+vnA==
+  dependencies:
+    "@parcel/config-default" "2.8.3"
+    "@parcel/core" "2.8.3"
+    "@parcel/diagnostic" "2.8.3"
+    "@parcel/events" "2.8.3"
+    "@parcel/fs" "2.8.3"
+    "@parcel/logger" "2.8.3"
+    "@parcel/package-manager" "2.8.3"
+    "@parcel/reporter-cli" "2.8.3"
+    "@parcel/reporter-dev-server" "2.8.3"
+    "@parcel/utils" "2.8.3"
+    chalk "^4.1.0"
+    commander "^7.0.0"
+    get-port "^4.2.0"
+    v8-compile-cache "^2.0.0"
+
+parent-module@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
+  integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+  dependencies:
+    callsites "^3.0.0"
+
+parse-json@^5.0.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
+  integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
+  dependencies:
+    "@babel/code-frame" "^7.0.0"
+    error-ex "^1.3.1"
+    json-parse-even-better-errors "^2.3.0"
+    lines-and-columns "^1.1.6"
+
+path-type@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
+  integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+
+picocolors@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+  integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+picomatch@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+postcss-value-parser@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
+  integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+posthtml-parser@^0.10.1:
+  version "0.10.2"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.10.2.tgz#df364d7b179f2a6bf0466b56be7b98fd4e97c573"
+  integrity sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==
+  dependencies:
+    htmlparser2 "^7.1.1"
+
+posthtml-parser@^0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.11.0.tgz#25d1c7bf811ea83559bc4c21c189a29747a24b7a"
+  integrity sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==
+  dependencies:
+    htmlparser2 "^7.1.1"
+
+posthtml-render@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-3.0.0.tgz#97be44931496f495b4f07b99e903cc70ad6a3205"
+  integrity sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==
+  dependencies:
+    is-json "^2.0.1"
+
+posthtml@^0.16.4, posthtml@^0.16.5:
+  version "0.16.6"
+  resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.16.6.tgz#e2fc407f67a64d2fa3567afe770409ffdadafe59"
+  integrity sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==
+  dependencies:
+    posthtml-parser "^0.11.0"
+    posthtml-render "^3.0.0"
+
+process@^0.11.10:
+  version "0.11.10"
+  resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+  integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==
+
+react-dom@^18.2.0:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
+  integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
+  dependencies:
+    loose-envify "^1.1.0"
+    scheduler "^0.23.0"
+
+react-error-overlay@6.0.9:
+  version "6.0.9"
+  resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
+  integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
+
+react-refresh@^0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf"
+  integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
+
+react@^18.2.0:
+  version "18.2.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
+  integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
+  dependencies:
+    loose-envify "^1.1.0"
+
+regenerator-runtime@^0.13.7:
+  version "0.13.11"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
+  integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
+resolve-from@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+  integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+safe-buffer@^5.0.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+scheduler@^0.23.0:
+  version "0.23.0"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
+  integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
+  dependencies:
+    loose-envify "^1.1.0"
+
+semver@^5.7.0, semver@^5.7.1:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+source-map-support@~0.5.20:
+  version "0.5.21"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
+  integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, 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==
+
+srcset@4:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/srcset/-/srcset-4.0.0.tgz#336816b665b14cd013ba545b6fe62357f86e65f4"
+  integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==
+
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
+supports-color@^5.3.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@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+svgo@^2.4.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
+  integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
+  dependencies:
+    "@trysound/sax" "0.2.0"
+    commander "^7.2.0"
+    css-select "^4.1.3"
+    css-tree "^1.1.3"
+    csso "^4.2.0"
+    picocolors "^1.0.0"
+    stable "^0.1.8"
+
+term-size@^2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
+  integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==
+
+terser@^5.2.0:
+  version "5.16.1"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880"
+  integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw==
+  dependencies:
+    "@jridgewell/source-map" "^0.3.2"
+    acorn "^8.5.0"
+    commander "^2.20.0"
+    source-map-support "~0.5.20"
+
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+tslib@^2.4.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
+  integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
+
+type-fest@^0.20.2:
+  version "0.20.2"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
+  integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
+
+typescript@>=3.0.0:
+  version "4.9.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
+  integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==
+
+update-browserslist-db@^1.0.9:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3"
+  integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==
+  dependencies:
+    escalade "^3.1.1"
+    picocolors "^1.0.0"
+
+utility-types@^3.10.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
+  integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
+
+v8-compile-cache@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
+
+weak-lru-cache@^1.2.2:
+  version "1.2.2"
+  resolved "https://registry.yarnpkg.com/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz#fdbb6741f36bae9540d12f480ce8254060dccd19"
+  integrity sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==
+
+xxhash-wasm@^0.4.2:
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz#752398c131a4dd407b5132ba62ad372029be6f79"
+  integrity sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA==
+
+yaml@^1.10.0:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
diff --git a/users/wpcarro/zoo/.envrc b/users/wpcarro/zoo/.envrc
new file mode 100644
index 000000000000..a4a62da526d3
--- /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 000000000000..fcae90c2987d
--- /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 000000000000..c18edbed9666
--- /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 000000000000..ba3f71d7c754
--- /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 000000000000..312a6cbd7689
--- /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 000000000000..5978d5b4d04a
--- /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 000000000000..b8553ace5532
--- /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 000000000000..0ee818b12f16
--- /dev/null
+++ b/users/zseri/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+zseri
diff --git a/users/zseri/dbwospof.md b/users/zseri/dbwospof.md
new file mode 100644
index 000000000000..f1d68cde069c
--- /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 000000000000..5a44eef09a54
--- /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 000000000000..836d90e88355
--- /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 000000000000..38f3fd64ecd7
--- /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 000000000000..b400c2782601
--- /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 000000000000..7395dec05e45
--- /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 000000000000..1832be00329c
--- /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 000000000000..9e21a7738a38
--- /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 000000000000..48100a628d7a
--- /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 000000000000..2520efd8363d
--- /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 000000000000..0f86a769fe63
--- /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 000000000000..79da0842c529
--- /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 000000000000..d33c73476b52
--- /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 000000000000..a6ebd93a7788
--- /dev/null
+++ b/views/README.md
@@ -0,0 +1,31 @@
+depot views
+===========
+
+This folder contains external views of depot content, defined using
+josh workspaces. See the individual views for a description of their
+individual content and usage information.
+
+Testing changes locally
+-----------------------
+
+Generally, when iterating on these files, it's best to locally invoke `josh-
+filter` (from `//third_party//josh`) locally to inspect how the workspace would
+look like:
+
+  - Commit your changes. This is required, as `josh-filter` operates on your
+    `HEAD`, not working directory state.
+  - Invoke `josh-filter` with the filter expression,
+    for example `josh-filter ':workspace=views/tvix'`.
+  - Peek at the synthesized git history by looking at `FILTERED_HEAD`.
+
+Testing changes in Gerrit
+-------------------------
+
+It's also possible to clone resulting workspaces for CLs that were already
+pushed to Gerrit, but didn't land in master yet.
+
+For CL1234 at revision 2, the URL passed to `git clone` would look like this:
+
+```
+https://code.tvl.fyi/depot.git@refs/changes/32/1234/2:workspace=views/kit.git
+````
diff --git a/views/default.nix b/views/default.nix
new file mode 100644
index 000000000000..83c2782719ca
--- /dev/null
+++ b/views/default.nix
@@ -0,0 +1,26 @@
+# Export configuration for the views.
+{ depot, pkgs, ... }:
+
+let
+  export-tvix = depot.tools.releases.filteredGitPush {
+    filter = ":workspace=views/tvix";
+    remote = "git@github.com:tvlfyi/tvix.git";
+    ref = "refs/heads/canon";
+  };
+
+  export-kit = depot.tools.releases.filteredGitPush {
+    filter = ":unsign:workspace=views/kit";
+    remote = "git@github.com:tvlfyi/kit.git";
+    ref = "refs/heads/canon";
+  };
+in
+(pkgs.runCommandLocal "export-views" { }
+  ''
+    echo "no-op carrier target for repo export steps" | tee $out
+  '').overrideAttrs
+  (_: {
+    meta.ci.extraSteps = {
+      inherit export-tvix export-kit;
+    };
+  })
+
diff --git a/views/kit/README.md b/views/kit/README.md
new file mode 100644
index 000000000000..95a38e738260
--- /dev/null
+++ b/views/kit/README.md
@@ -0,0 +1,26 @@
+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
+* `checks`: Collection of useful CI checks for Buildkite
+
+It can be accessed via git by cloning it as such:
+
+    git clone https://code.tvl.fyi/depot.git:unsign: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 000000000000..0bba63726db5
--- /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 000000000000..4e7e5955aa74
--- /dev/null
+++ b/views/kit/default.nix
@@ -0,0 +1,33 @@
+# 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 { };
+    externalArgs = args;
+  })
+, ...
+}@args:
+
+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;
+  };
+
+  checks = import ./checks { inherit pkgs; };
+  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 000000000000..202dee350dae
--- /dev/null
+++ b/views/kit/workspace.josh
@@ -0,0 +1,14 @@
+::LICENSE
+besadii = :/ops/besadii
+:/nix:[
+    ::buildGo/
+    ::buildkite/
+    ::lazy-deps/
+    ::readTree/
+]
+:/third_party:[
+    ::nixpkgs/
+    ::sources/
+]
+magrathea = :/tools/magrathea
+checks = :/tools/checks
diff --git a/views/tvix/buildkite.yml b/views/tvix/buildkite.yml
new file mode 100644
index 000000000000..4001ba9b9cdb
--- /dev/null
+++ b/views/tvix/buildkite.yml
@@ -0,0 +1,9 @@
+# Build pipeline for the filtered //views/tvix workspace of depot. This
+# pipeline is triggered by each build of canon.
+#
+# Pipeline status is visible on https://buildkite.com/tvl/tvix
+
+steps:
+  - label: ":crab: cargo build"
+    command: |
+      nix-shell --run "cargo build && cargo test"
diff --git a/views/tvix/default.nix b/views/tvix/default.nix
new file mode 100644
index 000000000000..33d6b6368668
--- /dev/null
+++ b/views/tvix/default.nix
@@ -0,0 +1,24 @@
+# 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.
+#
+{ pkgs ? (import ./nixpkgs {
+    depotOverlays = false;
+    depot.third_party.sources = import ./sources { };
+  })
+, ...
+}:
+
+pkgs.mkShell {
+  name = "tvix-rust-dev-env";
+  packages = [
+    pkgs.buf-language-server
+    pkgs.cargo
+    pkgs.clippy
+    pkgs.rust-analyzer
+    pkgs.rustc
+    pkgs.rustfmt
+    pkgs.protobuf
+    pkgs.evans
+  ];
+}
diff --git a/views/tvix/workspace.josh b/views/tvix/workspace.josh
new file mode 100644
index 000000000000..e0b15c7f30b5
--- /dev/null
+++ b/views/tvix/workspace.josh
@@ -0,0 +1,5 @@
+:/third_party:[
+    ::nixpkgs/
+    ::sources/
+]
+:/tvix
diff --git a/web/atom-feed/default.nix b/web/atom-feed/default.nix
new file mode 100644
index 000000000000..27c90a7b919c
--- /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) runCommand;
+
+  # '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 (runCommand "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 000000000000..29e65519ba35
--- /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 000000000000..442ff3d759d5
--- /dev/null
+++ b/web/atward/Cargo.lock
@@ -0,0 +1,814 @@
+# 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 = "android_system_properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e"
+dependencies = [
+ "libc",
+]
+
+[[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 = "bumpalo"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+
+[[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.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+dependencies = [
+ "iana-time-zone",
+ "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 = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[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.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys",
+]
+
+[[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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "js-sys"
+version = "0.3.59"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.132"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+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.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 = "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.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 = "once_cell"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
+
+[[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.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+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.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
+[[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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[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.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+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.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[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.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45"
+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.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 = "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.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
+dependencies = [
+ "tinyvec",
+]
+
+[[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.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a"
+
+[[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.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"
diff --git a/web/atward/Cargo.toml b/web/atward/Cargo.toml
new file mode 100644
index 000000000000..2ecd10f96f68
--- /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 000000000000..90a2f35cd1a9
--- /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 000000000000..f3ed04345c32
--- /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 000000000000..3af808b89831
--- /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 000000000000..eb2603a226c6
--- /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 000000000000..6033987f5b4b
--- /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 000000000000..a23f96ee9a74
--- /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 000000000000..66d7759deb7b
--- /dev/null
+++ b/web/blog/default.nix
@@ -0,0 +1,66 @@
+# 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;
+
+    # Optional name of the author to display.
+    author = option string;
+
+    # 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 000000000000..8c78b2cc928b
--- /dev/null
+++ b/web/blog/fragments.nix
@@ -0,0 +1,95 @@
+# 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) runCommand writeText;
+  inherit (depot.nix) renderMarkdown;
+
+  # 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: staticUrl: ''
+    <!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, staticUrl ? "https://static.tvl.fyi/${depot.web.static.drvHash}", ... }: post: runCommand "${post.key}.html" { } ''
+    cat ${writeText "header.html" (header name post.title staticUrl)} > $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 ""
+    }
+    ${if post ? author then "echo ' by ${post.author}' >> $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 000000000000..2e9580706346
--- /dev/null
+++ b/web/bubblegum/OWNERS
@@ -0,0 +1 @@
+sterni
diff --git a/web/bubblegum/README.md b/web/bubblegum/README.md
new file mode 100644
index 000000000000..48c2ba6e0328
--- /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 000000000000..528d73032ba6
--- /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 000000000000..76b91168b894
--- /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 000000000000..89482f93eacc
--- /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 000000000000..9a625afb55d7
--- /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 000000000000..bd4891f7d66d
--- /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 000000000000..3c58be7953f1
--- /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 000000000000..050586c3029c
--- /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/cgit-tvl/default.nix b/web/cgit-tvl/default.nix
new file mode 100644
index 000000000000..d26de9a5ebe7
--- /dev/null
+++ b/web/cgit-tvl/default.nix
@@ -0,0 +1,73 @@
+# Wrapper for running cgit through thttpd with TVL-specific
+# configuration.
+#
+# In practice this is only used for //ops/modules/cgit, but exposing
+# it here makes it easy to experiment with cgit locally.
+{ depot, lib, pkgs, ... }:
+
+let
+  cgitConfig = repo: pkgs.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=${repo}
+    repo.desc=monorepo for the virus lounge
+    repo.owner=The Virus Lounge
+    repo.clone-url=https://code.tvl.fyi/depot.git
+  '';
+
+  thttpdConfig = port: pkgs.writeText "thttpd.conf" ''
+    port=${toString 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 = repo: pkgs.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 repo}";
+         envp[envn++] = build_env( "PATH=%s", CGI_PATH );
+     #ifdef CGI_LD_LIBRARY_PATH
+  '';
+
+  thttpdCgit = repo: pkgs.thttpd.overrideAttrs (old: {
+    patches = [
+      ./thttpd_cgi_idx.patch
+      (thttpdConfigPatch repo)
+    ];
+  });
+
+in
+lib.makeOverridable
+  ({ port ? 2448
+   , repo ? "/var/lib/gerrit/git/depot.git/"
+   }: pkgs.writeShellScript "cgit-launch" ''
+    exec ${thttpdCgit repo}/bin/thttpd -D -C ${thttpdConfig port}
+  '')
+{ }
diff --git a/web/cgit-tvl/thttpd_cgi_idx.patch b/web/cgit-tvl/thttpd_cgi_idx.patch
new file mode 100644
index 000000000000..67dbc0c7ab80
--- /dev/null
+++ b/web/cgit-tvl/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/web/converse/.gitignore b/web/converse/.gitignore
new file mode 100644
index 000000000000..548206b0b297
--- /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 000000000000..651656b902e5
--- /dev/null
+++ b/web/converse/Cargo.lock
@@ -0,0 +1,3404 @@
+# 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.17",
+ "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.17",
+ "mio",
+ "net2",
+ "num_cpus",
+ "slab 0.4.7",
+ "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.17",
+ "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 0.6.1",
+ "slab 0.4.7",
+ "smallvec 0.6.14",
+ "time 0.1.45",
+ "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.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr 2.5.0",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[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.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
+
+[[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.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if 1.0.0",
+ "libc",
+ "miniz_oxide 0.5.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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[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 = "block-buffer"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+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.5.0",
+ "safemem",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
+[[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.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+
+[[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.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time 0.1.45",
+ "wasm-bindgen",
+ "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 = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[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.17",
+ "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.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be2018768ed1d848cc4d347d551546474025ba820e5db70e4c9aaa349f678bd7"
+dependencies = [
+ "aes-gcm",
+ "base64 0.13.1",
+ "hkdf",
+ "hmac",
+ "percent-encoding 2.2.0",
+ "rand 0.8.5",
+ "sha2",
+ "time 0.1.45",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+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-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[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.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2 0.4.7",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.59+curl-7.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "scratch",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "deflate"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f"
+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.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+dependencies = [
+ "block-buffer 0.10.3",
+ "crypto-common",
+]
+
+[[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.17",
+ "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.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+ "synstructure",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall 0.2.16",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "flate2"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide 0.6.2",
+]
+
+[[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.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding 2.2.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.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+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.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+
+[[package]]
+name = "gzip-header"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2"
+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.17",
+ "slab 0.4.7",
+ "string",
+ "tokio-io",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[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 0.9.0",
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
+dependencies = [
+ "crypto-mac",
+ "digest 0.9.0",
+]
+
+[[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.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[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.17",
+ "mime",
+ "net2",
+ "percent-encoding 1.0.1",
+ "relay",
+ "time 0.1.45",
+ "tokio-core",
+ "tokio-io",
+ "tokio-proto",
+ "tokio-service",
+ "unicase",
+ "want",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[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.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
+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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[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.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
+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.17",
+]
+
+[[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 = "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.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 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_oxide"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa"
+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.17",
+ "miow",
+ "net2",
+ "slab 0.4.7",
+ "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.17",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand 0.8.5",
+ "safemem",
+ "tempfile",
+ "twoway",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d0df99cfcd2530b2e694f6e17e7f37b8e26bb23983ac530c0c97408837c631"
+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.5.0",
+ "version_check 0.1.5",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+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 = "object"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+dependencies = [
+ "memchr 2.5.0",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[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.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132"
+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.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api 0.4.9",
+ "parking_lot_core 0.9.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.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec 1.10.0",
+ "windows-sys 0.42.0",
+]
+
+[[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.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[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.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[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.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[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.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2 1.0.47",
+]
+
+[[package]]
+name = "r2d2"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
+dependencies = [
+ "log 0.4.17",
+ "parking_lot 0.12.1",
+ "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.4",
+]
+
+[[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.4",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+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.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr 2.5.0",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[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.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f86e4c51a773f953f02bbab5fd049f004bfd384341d62da2a079aff812ab176"
+dependencies = [
+ "base64 0.13.1",
+ "brotli",
+ "chrono",
+ "deflate",
+ "filetime",
+ "multipart",
+ "num_cpus",
+ "percent-encoding 2.2.0",
+ "rand 0.8.5",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1 0.10.5",
+ "threadpool",
+ "time 0.3.17",
+ "tiny_http",
+ "url 2.3.1",
+]
+
+[[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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[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.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "scheduled-thread-pool"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf"
+dependencies = [
+ "parking_lot 0.12.1",
+]
+
+[[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 = "scratch"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+
+[[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.148"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.148"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+dependencies = [
+ "itoa 1.0.4",
+ "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"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.10.6",
+]
+
+[[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 0.9.0",
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest 0.9.0",
+ "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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
+[[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.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
+
+[[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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+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.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "unicode-ident",
+]
+
+[[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.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+ "unicode-xid 0.2.4",
+]
+
+[[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.16",
+ "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.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "time"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376"
+dependencies = [
+ "libc",
+ "num_threads",
+ "serde",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "tiny_http"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
+dependencies = [
+ "ascii",
+ "chunked_transfer",
+ "httpdate",
+ "log 0.4.17",
+]
+
+[[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 = "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.17",
+ "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.17",
+]
+
+[[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.17",
+ "mio",
+ "num_cpus",
+ "parking_lot 0.9.0",
+ "slab 0.4.7",
+ "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.17",
+ "num_cpus",
+ "slab 0.4.7",
+ "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.7",
+ "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.17",
+ "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.17",
+ "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.17",
+ "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.17",
+ "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.17",
+ "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.5.0",
+]
+
+[[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.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
+
+[[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.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna 0.3.0",
+ "percent-encoding 2.2.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.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[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.17",
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[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.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log 0.4.17",
+ "once_cell",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote 1.0.21",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[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 = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.0",
+ "windows_i686_gnu 0.42.0",
+ "windows_i686_msvc 0.42.0",
+ "windows_x86_64_gnu 0.42.0",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
+[[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_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
+[[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_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+
+[[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 000000000000..c77101144d5b
--- /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 000000000000..f288702d2fa1
--- /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 000000000000..e7a3f70aec07
--- /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 000000000000..89e3e6b1aba9
--- /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 000000000000..7a6583d31ea7
--- /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.pkg-config ];
+}
diff --git a/web/converse/envrc.example b/web/converse/envrc.example
new file mode 100644
index 000000000000..f255d269fec7
--- /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 000000000000..e69de29bb2d1
--- /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 000000000000..a9f526091194
--- /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 000000000000..d68895b1a7b7
--- /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 000000000000..8ebcef8e131a
--- /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 000000000000..7cf601fa2c54
--- /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 000000000000..cc7874b410ee
--- /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 000000000000..9713de6cfc4b
--- /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 000000000000..64dc07e0666a
--- /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 000000000000..afca8181cc56
--- /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 000000000000..8ad817908065
--- /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 000000000000..ad5beb9f6785
--- /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 000000000000..bb6528f33d78
--- /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 000000000000..07ff9a1196f6
--- /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 000000000000..a67ada3d4a57
--- /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 000000000000..74a559e35d00
--- /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 000000000000..c57e66290201
--- /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 000000000000..6b7d90eca63c
--- /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 000000000000..61fd222e18cf
--- /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 000000000000..fcb7133e8e41
--- /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 000000000000..0f14732f3845
--- /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 000000000000..280fef87003e
--- /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 000000000000..fb2a98c0af42
--- /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 000000000000..d7d4c44da201
--- /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 000000000000..1063fdc88224
--- /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 000000000000..87580a2f3f9e
--- /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 000000000000..a0d8915504d6
--- /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 000000000000..a4bd69023b8b
--- /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 000000000000..49f9dcf9745f
--- /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 000000000000..78d0241600b4
--- /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 000000000000..63b15fbed061
--- /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 000000000000..75e3eabc88f2
--- /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 000000000000..d06af12bd9f1
--- /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 000000000000..520af4342261
--- /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 000000000000..791932b87eaf
--- /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 000000000000..9ca7d97ba31d
--- /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 000000000000..b57fd899ea8d
--- /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 000000000000..07d6e7bfcf09
--- /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 000000000000..8ea2df1cd344
--- /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 000000000000..418440e028ce
--- /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 000000000000..66ec23de1f0a
--- /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 000000000000..379f10753ae5
--- /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/inbox.nix b/web/inbox.nix
new file mode 100644
index 000000000000..4bbf56300dc4
--- /dev/null
+++ b/web/inbox.nix
@@ -0,0 +1,81 @@
+# landing page for inbox.tvl.su
+
+{ depot, ... }:
+
+depot.web.tvl.template {
+  title = "TVL's public inbox";
+
+  # not hosted on whitby, so we need /latest
+  staticUrl = "https://static.tvl.su/latest";
+
+  extraHead = ''
+    <link rel="alternate" type="application/atom+xml" href="https://inbox.tvl.su/depot/new.atom" />
+  '';
+
+  content = ''
+    TVL's public inbox
+    ==================
+
+    This is the [public-inbox][] for [The Virus Lounge][TVL]. It is
+    essentially like a pull-based mailing list, where we discuss
+    anything related to our software or organisation, as well as
+    receive patches from external users.
+
+    ## Posting to the inbox
+
+    Anyone can send messages to the inbox by emailing
+    **depot@tvl.su**.
+
+    ## Accessing the inbox
+
+    There are several ways to access the inbox, depending on what is
+    most convenient for your personal email workflow.
+
+    ### Web browser
+
+    Go to [`/depot/`][inbox-html] to read the inbox in your web
+    browser. This is the easiest way to access messages, and with an
+    email client supporting `mailto:` links you can respond to
+    messages from there, too.
+
+    ### IMAP
+
+    The inbox is available via IMAP:
+
+    **Server:** `inbox.tvl.su`
+
+    **Port:** `993` (TLS enabled)
+
+    **Inbox:** `su.tvl.depot.0` (auto-discoverable)
+
+    You can use *any* credentials to log in, for example the username
+    `anonymous` with the password `kittens`. The server will just
+    ignore it.
+
+    TIP: There is a wrapper script in `//tools/fetch-depot-inbox` in
+    the TVL depot which you can use to synchronise the maildir to your
+    computer, which works for email clients like `notmuch`.
+
+    ### Atom feed
+
+    An Atom feed [is available][feed] and should work with your
+    favourite feed reader.
+
+    ### NNTP
+
+    News readers can access the inbox via NNTP:
+
+    **Server:** `inbox.tvl.su`
+
+    **Port:** `563` (TLS enabled)
+
+    **Group:** `su.tvl.depot.0` (auto-discoverable)
+
+    No credentials are required to access the server.
+
+    [public-inbox]: https://public-inbox.org/README.html
+    [TVL]: https://tvl.fyi
+    [inbox-html]: https://inbox.tvl.su/depot/
+    [feed]: https://inbox.tvl.su/depot/new.atom
+  '';
+}
diff --git a/web/panettone/.envrc b/web/panettone/.envrc
new file mode 100644
index 000000000000..be81feddb1a5
--- /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 000000000000..be303db03207
--- /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 000000000000..981c842111a8
--- /dev/null
+++ b/web/panettone/OWNERS
@@ -0,0 +1,3 @@
+grfn
+tazjin
+sterni
diff --git a/web/panettone/default.nix b/web/panettone/default.nix
new file mode 100644
index 000000000000..283f83499487
--- /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 000000000000..84723667e6b9
--- /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 000000000000..4d44e50fd3e8
--- /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 000000000000..483481ca9aa1
--- /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 000000000000..10aa5440d832
--- /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 000000000000..3ce07aa8d78d
--- /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 000000000000..aa753cb50fc5
--- /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 000000000000..66ea299858d3
--- /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 000000000000..e49293519bf4
--- /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 000000000000..2ab72a2e391e
--- /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 000000000000..c54a0ae474bf
--- /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 000000000000..4ff4c070f087
--- /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 000000000000..d87ac5ed4653
--- /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 000000000000..2abedf7b8fbb
--- /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 000000000000..bb907504368b
--- /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 000000000000..0224836cbc32
--- /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 000000000000..e4cd78a65a43
--- /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 000000000000..d2a2f974208e
--- /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 000000000000..ff52d916cb3a
--- /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 000000000000..9eaeb0ec1495
--- /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 000000000000..741cdbc64ff2
--- /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 000000000000..38e004543da6
--- /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 000000000000..4c0b94903fda
--- /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 000000000000..34b5c69ae1cf
--- /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 000000000000..84a008af7edb
--- /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 000000000000..85fd4687891e
--- /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 000000000000..d5b94cb9e7db
--- /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 000000000000..7cde9d96444b
--- /dev/null
+++ b/web/static/tvl.css
@@ -0,0 +1,198 @@
+/* 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
+}
+
+/* Blog Posts */
+
+article {
+    line-height: 1.5em;
+}
+
+/* spacing between the paragraphs in blog posts */
+article p {
+    margin: 1.4em auto;
+}
+
+/* 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;
+}
+
+code {
+    background: aliceblue;
+}
+
+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 000000000000..4712ad21bab0
--- /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
+    runCommand
+    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 (runCommand "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 000000000000..d4e0476c403a
--- /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 000000000000..4aac1b73147d
--- /dev/null
+++ b/web/tvl/blog/default.nix
@@ -0,0 +1,27 @@
+{ depot, ... }:
+
+{
+  config = {
+    name = "TVL's blog";
+    footer = depot.web.tvl.footer { };
+    baseUrl = "https://tvl.fyi/blog";
+  };
+
+  posts = builtins.sort (a: b: a.date > b.date) [
+    {
+      key = "rewriting-nix";
+      title = "Tvix: We are rewriting Nix";
+      date = 1638381387;
+      content = ./rewriting-nix.md;
+      author = "tazjin";
+    }
+
+    {
+      key = "tvix-status-september-22";
+      title = "Tvix Status - September '22";
+      date = 1662995534;
+      content = ./tvix-status-202209.md;
+      author = "tazjin";
+    }
+  ];
+}
diff --git a/web/tvl/blog/rewriting-nix.md b/web/tvl/blog/rewriting-nix.md
new file mode 100644
index 000000000000..1b143e5b0ed9
--- /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/blog/tvix-status-202209.md b/web/tvl/blog/tvix-status-202209.md
new file mode 100644
index 000000000000..b823820b0b70
--- /dev/null
+++ b/web/tvl/blog/tvix-status-202209.md
@@ -0,0 +1,165 @@
+We've now been working on our rewrite of Nix, [Tvix][], for over a
+year.
+
+As you can imagine, this past year has been turbulent, to say the
+least, given the regions where many of us live. As a result we haven't
+had as much time to work on fun things (like open-source software
+projects!) as we'd like.
+
+We've all been fortunate enough to continue making progress, but we
+just haven't had the bandwidth to communicate with you and keep you up
+to speed on what's going on. That's what this blog post is for.
+
+## Nix language evaluator
+
+The most significant progress in the past six months has been on our
+Nix language evaluator. To answer the most important question: yes,
+you can play with it right now – in [Tvixbolt][]!
+
+We got the evaluator into its current state by first listing all the
+problems we were likely to encounter, then solving them independently,
+and finally assembling all those small-scale solutions into a coherent
+whole. As a result, we briefly had an impractically large private
+source tree, which we have since [integrated][] into our monorepo.
+
+This process was much slower than we would have liked, due to code
+review bandwidth... which is to say, we're all volunteers. People have
+lives, bottlenecks happen.
+
+Most of this code was either written or reviewed by [grfn][],
+[sterni][] and [tazjin][] (that's me!).
+
+### How much of eval is working?
+
+*Most of it*! You can enter most (but not *all*, sorry! Not yet,
+anyway.) Nix language expressions in [Tvixbolt][] and observe how they
+are evaluated.
+
+There's a lot of interesting stuff going on under the hood, such as:
+
+* The Tvix compiler can emit warnings and errors without failing
+  early, and retains as much source information as possible. This will
+  enable you to use Tvix as the basis for developer tooling, such as
+  language servers.
+
+* The Tvix compiler performs in-depth scope analysis, so it can both
+  generate efficient bytecode for accessing identifiers, and alert you
+  about problems in your code before runtime.
+
+* The runtime supports tail-call optimisation in many (but – again –
+  not yet all) cases, so you can evaluate recursive expressions in
+  constant stack space.
+
+* The runtime can give you different backing representations for the
+  same Nix type. For example, an attribute set is represented
+  differently depending on whether you've constructed an empty one, a
+  `name/value` pair, or a larger set. This lets us optimise frequent,
+  well-known use-cases without impacting the general case much.
+
+We've run some initial benchmarks against C++ Nix (using the features
+that are ready), and in most cases Tvix evaluation is an order of
+magnitude faster. To be fair, though, these benchmarks are in no way
+indicative of real-life performance for things like `nixpkgs`. More
+information is coming... eventually.
+
+### How does it all work?
+
+Tvix's evaluator uses a custom abstract machine with a Nix-specific
+instruction set, and a compiler that traverses a parsed Nix AST to
+emit this bytecode and perform a set of optimisations and other
+analysis. The most important benefit of this is that we can plan and
+lay out the execution of a program in a way that is better suited to
+an efficient runtime than directly traversing the AST.
+
+TIP: You can see the generated bytecode in [Tvixbolt][]!
+
+This is all written in about 4000 lines of Rust (naturally), some of
+which – especially around scope-handling – are deceptively simple.
+
+As part of our CI suite, we run the evaluator against some tests we
+wrote ourselves, as well as against the upstream Nix test suite (which
+we don't *quite* pass yet. We're working on it!).
+
+### What's next for tvix-eval?
+
+Despite all our progress, there are still some unfinished feature
+areas, and some of them are pretty important:
+
+1. The majority of Nix's builtins – including fundamental ones like
+   `import` and `derivation` – aren't implemented yet.
+
+2. Neither are recursive attribute sets (`rec`). This isn't because of
+   a problem with the recursion itself, but because of the handling of
+   nested keys (such as `a.b`). We have a lackluster solution already,
+   but are designing a more efficient one.
+
+In both cases, we've mostly figured out what to do; now it's just a
+matter of finding the time to do it. Our progress is steady, and can
+be tracked [in the source][src] (viewer without Javascript
+[here][src-noscript]).
+
+Apart from that, the next steps are:
+
+* Comprehensive benchmarking. We're standing up an infrastructure for
+  continuous benchmarking to measure the impact of changes. It'll also
+  let us identify and optimise hotspots
+
+* Implementing known optimisations. There are some areas of the code
+  that have the potential for significant speed gains, but we're
+  holding off implementing those until the evaluator is feature
+  complete and passes the Nix test suite.
+
+* Finishing our language specification. Based on what we've learned,
+  we're writing a specification of the Nix language that captures its
+  various behaviours in all their tricky subtlety and subtle trickery.
+
+Once we can evaluate `nixpkgs`, we're likely to shift our focus
+towards the other areas of Tvix.
+
+## The Other Areas of Tvix
+
+Speaking of these other areas (most importantly, the builder and store
+implementation), we've made some nice progress there also.
+
+While we've yet to start assembling the actual pieces, [flokli][] and
+[adisbladis][] have been hard at work on [go-nix][], which aims to
+implement many of the low-level primitives required for the Nix store
+and builder (hashing and encoding schemes, archive formats, reference
+scanning ...).
+
+We're looking forward to telling you more in the next Tvix status
+update!
+
+## Outro ...
+
+We'd be delighted to onboard new contributors to Tvix! Please take a
+look at the main [TVL page](https://tvl.fyi) to find out how to get in
+touch with us if you'd like to join!
+
+Thanks also, of course, to [NLNet](https://nlnet.nl/) for sponsoring
+some of this work!
+
+And finally, we would like to thank and pay our respects to jD91mZM2 –
+the original author of
+[rnix-parser](https://github.com/nix-community/rnix-parser) – who has
+sadly passed away. Please, tell people how important they are to you.
+
+We use `rnix-parser` in our compiler, and its well-designed internals
+(also thanks to its new maintainers!) have saved us a lot of time.
+
+That's it for this update. Go play with [Tvixbolt][], have fun
+figuring out weird ways to break it – and if you do, let us know.
+
+We'll see you around!
+
+[Tvix]: https://tvl.fyi/blog/rewriting-nix
+[Tvixbolt]: https://tvixbolt.tvl.su
+[integrated]: https://cl.tvl.fyi/q/status:merged+%2522tvix/eval%2522+mergedbefore:2022-09-09
+[src]: https://cs.tvl.fyi/depot/-/tree/tvix/eval
+[src-noscript]: https://code.tvl.fyi/tree/tvix/eval
+[tazjin]: https://tazj.in
+[grfn]: https://gws.fyi/
+[sterni]: https://github.com/sternenseemann
+[go-nix]: https://github.com/nix-community/go-nix
+[flokli]: https://flokli.de/
+[adisbladis]: https://github.com/adisbladis
diff --git a/web/tvl/default.nix b/web/tvl/default.nix
new file mode 100644
index 000000000000..8bbc7d566ab7
--- /dev/null
+++ b/web/tvl/default.nix
@@ -0,0 +1,141 @@
+{ depot, lib, pkgs, ... }:
+
+with depot.nix.yants;
+
+let
+  inherit (builtins) filter;
+  inherit (pkgs) graphviz runCommand 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 = runCommand "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.
+
+      ----------------
+
+      ## Blog
+
+      Here are the most recent TVL blog posts.
+
+      ${listPosts publishedPosts}
+
+      You can also follow our [atom feed](https://tvl.fyi/feed.atom).
+
+      ----------------
+
+      ## Getting in touch
+
+      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
+
+      Discussions of our software, patches, and anything else really
+      can also be sent to us via email to **depot@tvl.su**. You can
+      see the mails submitted to that list in our [public inbox][].
+
+      [public inbox]: https://inbox.tvl.su
+
+      ----------------
+
+      ## 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
+runCommand "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 000000000000..dc2c963f90f4
--- /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 000000000000..808413549218
--- /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.runCommand "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 000000000000..d35971e0a91a
--- /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 000000000000..50ddc31e73d8
--- /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) runCommand lib;
+  inherit (depot.tools) cheddar;
+in
+
+runCommand "${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 000000000000..e1b690d2684c
--- /dev/null
+++ b/web/tvl/tvl.dot
@@ -0,0 +1,174 @@
+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/"];
+    amjoseph;
+    andi [label="andi-" href="https://andreas.rammhold.de/"];
+    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/"];
+    etu [href="https://elis.nu/"];
+    ezemtsov [href="https://github.com/ezemtsov"];
+    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"];
+    implr [href="https://twitter.com/implring"];
+    isomer [href="https://www.lorier.net/"];
+    j4m3s [href="https://github.com/j4m3s-s"];
+    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"];
+    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/"];
+    tvix [href="https://code.tvl.fyi/tree/tvix"];
+    ircv3 [href="https://ircv3.net/"];
+    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 -> TVL;
+    tvix -> TVL;
+
+    spotify -> tazjin;
+    google -> tazjin;
+    eve -> tazjin;
+    unspecific -> tazjin;
+    edef -> tazjin;
+    ezemtsov -> tazjin;
+
+    // via nix
+    adisbladis -> nix;
+    jusrin -> nix;
+    ghuntley -> nix;
+    flokli -> nix;
+    andi -> nix;
+    Profpatsch -> nix;
+    lassulus -> nix;
+    etu -> nix;
+
+    // via tvix
+    j4m3s -> tvix;
+    amjoseph -> tvix;
+
+    // via edef
+    benjojo -> edef;
+    espes -> edef;
+    firefly -> edef;
+    leah2 -> aurora;
+    ncl -> edef;
+    qyliss -> edef;
+
+    // via spotify
+    seven -> spotify;
+
+    // via google
+    Irenes -> google;
+    isomer -> google;
+    lukegb -> google;
+    wpcarro -> google;
+    fzakaria -> google;
+    mdjnsn -> google;
+
+    // random primary
+    grfn -> wpcarro;
+    aurora -> eve;
+    cynthia -> benjojo;
+    eta -> unspecific;
+    ericvolp -> lukegb;
+    marcusr -> unspecific;
+    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"];
+
+    // 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;
+    ezemtsov -> nix;
+  }
+
+  // baby
+  subgraph {
+    edge [weight=0 style="dotted" color="pink" arrowhead="none"];
+    cynthia -> baby;
+    eta -> baby;
+  }
+}