diff --git a/go.mod b/go.mod
index 5f90c83..e8bc5ba 100644
--- a/go.mod
+++ b/go.mod
@@ -40,6 +40,7 @@ require (
github.com/charmbracelet/x/ansi v0.8.0
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
diff --git a/go.sum b/go.sum
index ec3d8c0..448b055 100644
--- a/go.sum
+++ b/go.sum
@@ -40,6 +40,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
+github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
diff --git a/internal/webserver/webserver.go b/internal/webserver/webserver.go
new file mode 100644
index 0000000..0e52515
--- /dev/null
+++ b/internal/webserver/webserver.go
@@ -0,0 +1,877 @@
+package webserver
+
+import (
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "strings"
+
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
+
+ "github.com/kyren223/eko/embeds"
+)
+
+func ServeEkoWebsite() {
+ slog.Info("starting web-server...")
+
+ http.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/css")
+ w.Write([]byte(css))
+ })
+
+ http.HandleFunc("/terms-of-service", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ tos := embeds.TermsOfService.Load().(string)
+ tos = strings.ReplaceAll(tos, "Privacy Policy", "[Privacy Policy](../privacy-policy)")
+ html := mdToHTML(tos)
+ writeLegalLayoutHtml(w, html)
+ })
+
+ http.HandleFunc("/privacy-policy", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ privacy := embeds.PrivacyPolicy.Load().(string)
+ privacy = strings.ReplaceAll(privacy, "Terms of Service", "[Terms of Service](../terms-of-service)")
+ html := mdToHTML(privacy)
+ writeLegalLayoutHtml(w, html)
+ })
+
+ slog.Error("webserver error", "error", http.ListenAndServe(":7443", nil))
+}
+
+func writeLegalLayoutHtml(w io.Writer, html string) {
+ fmt.Fprintf(w, `
+
+
+
+
+ Terms of Service
+
+
+
+
+
+ %s
+
+
+
+
+ `, html)
+}
+
+func mdToHTML(md string) string {
+ // create markdown parser with extensions
+ extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
+ p := parser.NewWithExtensions(extensions)
+ doc := p.Parse([]byte(md))
+
+ // create HTML renderer with extensions
+ htmlFlags := html.CommonFlags | html.HrefTargetBlank
+ opts := html.RendererOptions{Flags: htmlFlags}
+ renderer := html.NewRenderer(opts)
+
+ return string(markdown.Render(doc, renderer))
+}
+
+// Yes this is horrible, but it works and I don't like web dev :)
+// Feel free to improve it and open a PR
+// Probably most of these are unncessary, I just copy pasted it from my website css
+var css = `
+.sl-markdown-content
+ :not(.expressive-code *)
+ + :not(a, strong, em, del, span, input, code, br)
+ + :not(a, strong, em, del, span, input, code, br, :where(.not-content *)) {
+ margin-top: 1rem;
+}
+
+/* Headings after non-headings have more spacing. */
+.sl-markdown-content
+ :not(h1, h2, h3, h4, h5, h6)
+ + :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
+ margin-top: 1.5em;
+}
+
+.sl-markdown-content li + li:not(:where(.not-content *)),
+.sl-markdown-content dt + dt:not(:where(.not-content *)),
+.sl-markdown-content dt + dd:not(:where(.not-content *)),
+.sl-markdown-content dd + dd:not(:where(.not-content *)) {
+ margin-top: 0.25rem;
+}
+
+.sl-markdown-content li:not(:where(.not-content *)) {
+ overflow-wrap: anywhere;
+}
+
+.sl-markdown-content
+ li
+ > :last-child:not(li, ul, ol):not(
+ a,
+ strong,
+ em,
+ del,
+ span,
+ input,
+ :where(.not-content *)
+ ) {
+ margin-bottom: 1.25rem;
+}
+
+.sl-markdown-content dt:not(:where(.not-content *)) {
+ font-weight: 700;
+}
+.sl-markdown-content dd:not(:where(.not-content *)) {
+ padding-inline-start: 1rem;
+}
+
+.sl-markdown-content :is(h1, h2, h3, h4, h5, h6):not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+ line-height: var(--sl-line-height-headings);
+ font-weight: 600;
+ font-size: 10000%;
+}
+
+.sl-markdown-content
+ :is(img, picture, video, canvas, svg, iframe):not(:where(.not-content *)) {
+ display: block;
+ max-width: 100%;
+ height: auto;
+}
+
+.sl-markdown-content h1:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h1);
+}
+.sl-markdown-content h2:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h2);
+}
+.sl-markdown-content h3:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h3);
+}
+.sl-markdown-content h4:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h4);
+}
+.sl-markdown-content h5:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h5);
+}
+.sl-markdown-content h6:not(:where(.not-content *)) {
+ font-size: var(--sl-text-h6);
+}
+
+.sl-markdown-content a:not(:where(.not-content *)) {
+ color: var(--sl-color-text-accent);
+}
+.sl-markdown-content a:hover:not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+}
+
+.sl-markdown-content code:not(:where(.not-content *)) {
+ background-color: var(--sl-color-bg-inline-code);
+ margin-block: -0.125rem;
+ padding: 0.125rem 0.375rem;
+ font-size: var(--sl-text-code-sm);
+}
+.sl-markdown-content :is(h1, h2, h3, h4, h5, h6) code {
+ font-size: inherit;
+}
+
+.sl-markdown-content pre:not(:where(.not-content *)) {
+ border: 1px solid var(--sl-color-gray-5);
+ padding: 0.75rem 1rem;
+ font-size: var(--sl-text-code);
+ tab-size: 2;
+}
+
+.sl-markdown-content pre code:not(:where(.not-content *)) {
+ all: unset;
+ font-family: var(--__sl-font-mono);
+}
+
+.sl-markdown-content blockquote:not(:where(.not-content *)) {
+ border-inline-start: 1px solid var(--sl-color-gray-5);
+ padding-inline-start: 1rem;
+}
+
+/* Table styling */
+.sl-markdown-content table:not(:where(.not-content *)) {
+ display: block;
+ overflow: auto;
+ border-spacing: 0;
+}
+.sl-markdown-content :is(th, td):not(:where(.not-content *)) {
+ border-bottom: 1px solid var(--sl-color-gray-5);
+ padding: 0.5rem 1rem;
+ /* Align text to the top of the row in multiline tables. */
+ vertical-align: baseline;
+}
+.sl-markdown-content
+ :is(th:first-child, td:first-child):not(:where(.not-content *)) {
+ padding-inline-start: 0;
+}
+.sl-markdown-content
+ :is(th:last-child, td:last-child):not(:where(.not-content *)) {
+ padding-inline-end: 0;
+}
+.sl-markdown-content th:not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+ font-weight: 600;
+}
+/* Align headings to the start of the line unless set by the align attribute. */
+.sl-markdown-content th:not([align]):not(:where(.not-content *)) {
+ text-align: start;
+}
+/* s,
s, and s inside asides */
+.sl-markdown-content
+ .starlight-aside
+ :is(th, td, hr, blockquote):not(:where(.not-content *)) {
+ border-color: var(--sl-color-gray-4);
+}
+@supports (
+ border-color:
+ color-mix(in srgb, var(--sl-color-asides-text-accent) 30%, transparent)
+) {
+ .sl-markdown-content
+ .starlight-aside
+ :is(th, td, hr, blockquote):not(:where(.not-content *)) {
+ border-color: color-mix(
+ in srgb,
+ var(--sl-color-asides-text-accent) 30%,
+ transparent
+ );
+ }
+}
+
+/* inside asides */
+@supports (
+ border-color:
+ color-mix(in srgb, var(--sl-color-asides-text-accent) 12%, transparent)
+) {
+ .sl-markdown-content .starlight-aside code:not(:where(.not-content *)) {
+ background-color: color-mix(
+ in srgb,
+ var(--sl-color-asides-text-accent) 12%,
+ transparent
+ );
+ }
+}
+
+.sl-markdown-content hr:not(:where(.not-content *)) {
+ border: 0;
+ border-bottom: 1px solid var(--sl-color-hairline);
+}
+
+/* and styles */
+.sl-markdown-content details:not(:where(.not-content *)) {
+ --sl-details-border-color: var(--sl-color-gray-5);
+ --sl-details-border-color--hover: var(--sl-color-text-accent);
+
+ border-inline-start: 2px solid var(--sl-details-border-color);
+ padding-inline-start: 1rem;
+}
+.sl-markdown-content details:not([open]):hover:not(:where(.not-content *)),
+.sl-markdown-content details:has(> summary:hover):not(:where(.not-content *)) {
+ border-color: var(--sl-details-border-color--hover);
+}
+.sl-markdown-content summary:not(:where(.not-content *)) {
+ color: var(--sl-color-white);
+ cursor: pointer;
+ display: block; /* Needed to hide the default marker in some browsers. */
+ font-weight: 600;
+ /* Expand the outline so that the marker cannot distort it. */
+ margin-inline-start: -0.5rem;
+ padding-inline-start: 0.5rem;
+}
+.sl-markdown-content details[open] > summary:not(:where(.not-content *)) {
+ margin-bottom: 1rem;
+}
+
+/* marker styles */
+.sl-markdown-content summary:not(:where(.not-content *))::marker,
+.sl-markdown-content
+ summary:not(:where(.not-content *))::-webkit-details-marker {
+ display: none;
+}
+.sl-markdown-content summary:not(:where(.not-content *))::before {
+ --sl-details-marker-size: 1.25rem;
+
+ background-color: currentColor;
+ content: "";
+ display: inline-block;
+ height: var(--sl-details-marker-size);
+ width: var(--sl-details-marker-size);
+ margin-inline: calc((var(--sl-details-marker-size) / 4) * -1) 0.25rem;
+ vertical-align: middle;
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M14.8 11.3 10.6 7a1 1 0 1 0-1.4 1.5l3.5 3.5-3.5 3.5a1 1 0 0 0 0 1.4 1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3l4.2-4.2a1 1 0 0 0 0-1.4Z'/%3E%3C/svg%3E%0A");
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M14.8 11.3 10.6 7a1 1 0 1 0-1.4 1.5l3.5 3.5-3.5 3.5a1 1 0 0 0 0 1.4 1 1 0 0 0 .7.3 1 1 0 0 0 .7-.3l4.2-4.2a1 1 0 0 0 0-1.4Z'/%3E%3C/svg%3E%0A");
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+}
+@media (prefers-reduced-motion: no-preference) {
+ .sl-markdown-content summary:not(:where(.not-content *))::before {
+ transition: transform 0.2s ease-in-out;
+ }
+}
+.sl-markdown-content
+ details[open]
+ > summary:not(:where(.not-content *))::before {
+ transform: rotateZ(90deg);
+}
+[dir="rtl"] .sl-markdown-content summary:not(:where(.not-content *))::before,
+.sl-markdown-content [dir="rtl"] summary:not(:where(.not-content *))::before {
+ transform: rotateZ(180deg);
+}
+/* with only a paragraph automatically added when using MDX */
+.sl-markdown-content summary:not(:where(.not-content *)) p:only-child {
+ display: inline;
+}
+
+/* styles inside asides */
+.sl-markdown-content .starlight-aside details:not(:where(.not-content *)) {
+ --sl-details-border-color: var(--sl-color-asides-border);
+ --sl-details-border-color--hover: var(--sl-color-asides-text-accent);
+}
+
+
+.starlight-aside {
+ padding: 1rem;
+ border-inline-start: 0.25rem solid var(--sl-color-asides-border);
+ color: var(--sl-color-white);
+}
+.starlight-aside--note {
+ --sl-color-asides-text-accent: var(--sl-color-blue-high);
+ --sl-color-asides-border: var(--sl-color-blue);
+ background-color: var(--sl-color-blue-low);
+}
+.starlight-aside--tip {
+ --sl-color-asides-text-accent: var(--sl-color-purple-high);
+ --sl-color-asides-border: var(--sl-color-purple);
+ background-color: var(--sl-color-purple-low);
+}
+.starlight-aside--caution {
+ --sl-color-asides-text-accent: var(--sl-color-orange-high);
+ --sl-color-asides-border: var(--sl-color-orange);
+ background-color: var(--sl-color-orange-low);
+}
+.starlight-aside--danger {
+ --sl-color-asides-text-accent: var(--sl-color-red-high);
+ --sl-color-asides-border: var(--sl-color-red);
+ background-color: var(--sl-color-red-low);
+}
+
+.starlight-aside__title {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ font-size: var(--sl-text-h5);
+ font-weight: 600;
+ line-height: var(--sl-line-height-headings);
+ color: var(--sl-color-asides-text-accent);
+}
+
+.starlight-aside__icon {
+ font-size: 1.333em;
+ width: 1em;
+ height: 1em;
+}
+
+.starlight-aside__title + .starlight-aside__content {
+ margin-top: 0.5rem;
+}
+
+.starlight-aside__content a {
+ color: var(--sl-color-asides-text-accent);
+}
+
+:root,
+::backdrop {
+ /* Colors (dark mode) */
+ --sl-color-white: hsl(0, 0%, 100%); /* “white” */
+ --sl-color-gray-1: hsl(224, 20%, 94%);
+ --sl-color-gray-2: hsl(224, 6%, 77%);
+ --sl-color-gray-3: hsl(224, 6%, 56%);
+ --sl-color-gray-4: hsl(224, 7%, 36%);
+ --sl-color-gray-5: hsl(224, 10%, 23%);
+ --sl-color-gray-6: hsl(224, 14%, 16%);
+ --sl-color-black: hsl(224, 10%, 10%);
+
+ --sl-hue-orange: 41;
+ --sl-color-orange-low: hsl(var(--sl-hue-orange), 39%, 22%);
+ --sl-color-orange: hsl(var(--sl-hue-orange), 82%, 63%);
+ --sl-color-orange-high: hsl(var(--sl-hue-orange), 82%, 87%);
+ --sl-hue-green: 101;
+ --sl-color-green-low: hsl(var(--sl-hue-green), 39%, 22%);
+ --sl-color-green: hsl(var(--sl-hue-green), 82%, 63%);
+ --sl-color-green-high: hsl(var(--sl-hue-green), 82%, 80%);
+ --sl-hue-blue: 234;
+ --sl-color-blue-low: hsl(var(--sl-hue-blue), 54%, 20%);
+ --sl-color-blue: hsl(var(--sl-hue-blue), 100%, 60%);
+ --sl-color-blue-high: hsl(var(--sl-hue-blue), 100%, 87%);
+ --sl-hue-purple: 281;
+ --sl-color-purple-low: hsl(var(--sl-hue-purple), 39%, 22%);
+ --sl-color-purple: hsl(var(--sl-hue-purple), 82%, 63%);
+ --sl-color-purple-high: hsl(var(--sl-hue-purple), 82%, 89%);
+ --sl-hue-red: 339;
+ --sl-color-red-low: hsl(var(--sl-hue-red), 39%, 22%);
+ --sl-color-red: hsl(var(--sl-hue-red), 82%, 63%);
+ --sl-color-red-high: hsl(var(--sl-hue-red), 82%, 87%);
+
+ --sl-color-accent-low: hsl(224, 54%, 20%);
+ --sl-color-accent: hsl(224, 100%, 60%);
+ --sl-color-accent-high: hsl(224, 100%, 85%);
+
+ --sl-color-text: var(--sl-color-gray-2);
+ --sl-color-text-accent: var(--sl-color-accent-high);
+ --sl-color-text-invert: var(--sl-color-accent-low);
+ --sl-color-bg: var(--sl-color-black);
+ --sl-color-bg-nav: var(--sl-color-gray-6);
+ --sl-color-bg-sidebar: var(--sl-color-gray-6);
+ --sl-color-bg-inline-code: var(--sl-color-gray-5);
+ --sl-color-bg-accent: var(--sl-color-accent-high);
+ --sl-color-hairline-light: var(--sl-color-gray-5);
+ --sl-color-hairline: var(--sl-color-gray-6);
+ --sl-color-hairline-shade: var(--sl-color-black);
+
+ --sl-color-backdrop-overlay: hsla(223, 13%, 10%, 0.66);
+
+ /* Shadows (dark mode) */
+ --sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.12),
+ 0px 2px 1px hsla(0, 0%, 0%, 0.24);
+ --sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.08),
+ 0px 5px 2px hsla(0, 0%, 0%, 0.08), 0px 3px 2px hsla(0, 0%, 0%, 0.12),
+ 0px 1px 1px hsla(0, 0%, 0%, 0.15);
+ --sl-shadow-lg: 0px 25px 7px hsla(0, 0%, 0%, 0.03),
+ 0px 16px 6px hsla(0, 0%, 0%, 0.1), 0px 9px 5px hsla(223, 13%, 10%, 0.33),
+ 0px 4px 4px hsla(0, 0%, 0%, 0.75), 0px 4px 2px hsla(0, 0%, 0%, 0.25);
+
+ /* Text size and line height */
+ --sl-text-2xs: 0.75rem; /* 12px */
+ --sl-text-xs: 0.8125rem; /* 13px */
+ --sl-text-sm: 0.875rem; /* 14px */
+ --sl-text-base: 1rem; /* 16px */
+ --sl-text-lg: 1.125rem; /* 18px */
+ --sl-text-xl: 1.25rem; /* 20px */
+ --sl-text-2xl: 1.5rem; /* 24px */
+ --sl-text-3xl: 1.8125rem; /* 29px */
+ --sl-text-4xl: 2.1875rem; /* 35px */
+ --sl-text-5xl: 2.625rem; /* 42px */
+ --sl-text-6xl: 4rem; /* 64px */
+
+ --sl-text-body: var(--sl-text-base);
+ --sl-text-body-sm: var(--sl-text-xs);
+ --sl-text-code: var(--sl-text-sm);
+ --sl-text-code-sm: var(--sl-text-xs);
+ --sl-text-h1: var(--sl-text-4xl);
+ --sl-text-h2: var(--sl-text-3xl);
+ --sl-text-h3: var(--sl-text-2xl);
+ --sl-text-h4: var(--sl-text-xl);
+ --sl-text-h5: var(--sl-text-lg);
+
+ --sl-line-height: 1.75;
+ --sl-line-height-headings: 1.2;
+
+ --sl-font-system: ui-sans-serif, 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";
+ --sl-font-system-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
+ "Liberation Mono", "Courier New", monospace;
+ --__sl-font: var(--sl-font, var(--sl-font-system)), var(--sl-font-system);
+ --__sl-font-mono: var(--sl-font-mono, var(--sl-font-system-mono)),
+ var(--sl-font-system-mono);
+
+ /** Key layout values */
+ --sl-nav-height: 3.5rem;
+ --sl-nav-pad-x: 1rem;
+ --sl-nav-pad-y: 0.75rem;
+ --sl-mobile-toc-height: 3rem;
+ --sl-sidebar-width: 18.75rem;
+ --sl-sidebar-pad-x: 1rem;
+ --sl-content-width: 45rem;
+ --sl-content-pad-x: 1rem;
+ --sl-menu-button-size: 2rem;
+ --sl-nav-gap: var(--sl-content-pad-x);
+ /* Offset required to show outline inside an element instead of round the outside */
+ --sl-outline-offset-inside: -0.1875rem;
+
+ /* Global z-index values */
+ --sl-z-index-toc: 4;
+ --sl-z-index-menu: 5;
+ --sl-z-index-navbar: 10;
+ --sl-z-index-skiplink: 20;
+}
+
+:root[data-theme="light"],
+[data-theme="light"] ::backdrop {
+ /* Colours (light mode) */
+ --sl-color-white: hsl(224, 10%, 10%);
+ --sl-color-gray-1: hsl(224, 14%, 16%);
+ --sl-color-gray-2: hsl(224, 10%, 23%);
+ --sl-color-gray-3: hsl(224, 7%, 36%);
+ --sl-color-gray-4: hsl(224, 6%, 56%);
+ --sl-color-gray-5: hsl(224, 6%, 77%);
+ --sl-color-gray-6: hsl(224, 20%, 94%);
+ --sl-color-gray-7: hsl(224, 19%, 97%);
+ --sl-color-black: hsl(0, 0%, 100%);
+
+ --sl-color-orange-high: hsl(var(--sl-hue-orange), 80%, 25%);
+ --sl-color-orange: hsl(var(--sl-hue-orange), 90%, 60%);
+ --sl-color-orange-low: hsl(var(--sl-hue-orange), 90%, 88%);
+ --sl-color-green-high: hsl(var(--sl-hue-green), 80%, 22%);
+ --sl-color-green: hsl(var(--sl-hue-green), 90%, 46%);
+ --sl-color-green-low: hsl(var(--sl-hue-green), 85%, 90%);
+ --sl-color-blue-high: hsl(var(--sl-hue-blue), 80%, 30%);
+ --sl-color-blue: hsl(var(--sl-hue-blue), 90%, 60%);
+ --sl-color-blue-low: hsl(var(--sl-hue-blue), 88%, 90%);
+ --sl-color-purple-high: hsl(var(--sl-hue-purple), 90%, 30%);
+ --sl-color-purple: hsl(var(--sl-hue-purple), 90%, 60%);
+ --sl-color-purple-low: hsl(var(--sl-hue-purple), 80%, 90%);
+ --sl-color-red-high: hsl(var(--sl-hue-red), 80%, 30%);
+ --sl-color-red: hsl(var(--sl-hue-red), 90%, 60%);
+ --sl-color-red-low: hsl(var(--sl-hue-red), 80%, 90%);
+
+ --sl-color-accent-high: hsl(234, 80%, 30%);
+ --sl-color-accent: hsl(234, 90%, 60%);
+ --sl-color-accent-low: hsl(234, 88%, 90%);
+
+ --sl-color-text-accent: var(--sl-color-accent);
+ --sl-color-text-invert: var(--sl-color-black);
+ --sl-color-bg-nav: var(--sl-color-gray-7);
+ --sl-color-bg-sidebar: var(--sl-color-bg);
+ --sl-color-bg-inline-code: var(--sl-color-gray-6);
+ --sl-color-bg-accent: var(--sl-color-accent);
+ --sl-color-hairline-light: var(--sl-color-gray-6);
+ --sl-color-hairline-shade: var(--sl-color-gray-6);
+
+ --sl-color-backdrop-overlay: hsla(225, 9%, 36%, 0.66);
+
+ /* Shadows (light mode) */
+ --sl-shadow-sm: 0px 1px 1px hsla(0, 0%, 0%, 0.06),
+ 0px 2px 1px hsla(0, 0%, 0%, 0.06);
+ --sl-shadow-md: 0px 8px 4px hsla(0, 0%, 0%, 0.03),
+ 0px 5px 2px hsla(0, 0%, 0%, 0.03), 0px 3px 2px hsla(0, 0%, 0%, 0.06),
+ 0px 1px 1px hsla(0, 0%, 0%, 0.06);
+ --sl-shadow-lg: 0px 25px 7px rgba(0, 0, 0, 0.01),
+ 0px 16px 6px hsla(0, 0%, 0%, 0.03), 0px 9px 5px hsla(223, 13%, 10%, 0.08),
+ 0px 4px 4px hsla(0, 0%, 0%, 0.16), 0px 4px 2px hsla(0, 0%, 0%, 0.04);
+}
+
+@media (min-width: 50em) {
+ :root {
+ --sl-nav-height: 4rem;
+ --sl-nav-pad-x: 1.5rem;
+ --sl-text-h1: var(--sl-text-5xl);
+ --sl-text-h2: var(--sl-text-4xl);
+ --sl-text-h3: var(--sl-text-3xl);
+ --sl-text-h4: var(--sl-text-2xl);
+ }
+}
+
+@media (min-width: 72rem) {
+ :root {
+ --sl-content-pad-x: 1.5rem;
+ --sl-mobile-toc-height: 0rem;
+ }
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+* {
+ margin: 0;
+}
+
+html {
+ color-scheme: dark;
+ accent-color: var(--sl-color-accent);
+}
+
+html[data-theme="light"] {
+ color-scheme: light;
+}
+
+body {
+ font-family: var(--__sl-font);
+ line-height: var(--sl-line-height);
+ -webkit-font-smoothing: antialiased;
+ color: var(--sl-color-text);
+ background-color: var(--sl-color-bg);
+}
+
+input,
+button,
+textarea,
+select {
+ font: inherit;
+}
+
+p,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+code {
+ overflow-wrap: anywhere;
+}
+
+code {
+ font-family: var(--__sl-font-mono);
+}
+
+:root {
+ --astro-code-color-text: var(--sl-color-white);
+ --astro-code-color-background: var(--sl-color-gray-6);
+ --astro-code-token-constant: var(--sl-color-blue-high);
+ --astro-code-token-string: var(--sl-color-green-high);
+ --astro-code-token-comment: var(--sl-color-gray-2);
+ --astro-code-token-keyword: var(--sl-color-purple-high);
+ --astro-code-token-parameter: var(--sl-color-red-high);
+ --astro-code-token-function: var(--sl-color-red-high);
+ --astro-code-token-string-expression: var(--sl-color-green-high);
+ --astro-code-token-punctuation: var(--sl-color-gray-2);
+ --astro-code-token-link: var(--sl-color-blue-high);
+}
+
+.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;
+}
+
+.sl-hidden {
+ display: none;
+}
+.sl-flex {
+ display: flex;
+}
+.sl-block {
+ display: block;
+}
+@media (min-width: 50rem) {
+ .md\:sl-hidden {
+ display: none;
+ }
+ .md\:sl-flex {
+ display: flex;
+ }
+ .md\:sl-block {
+ display: block;
+ }
+}
+@media (min-width: 72rem) {
+ .lg\:sl-hidden {
+ display: none;
+ }
+ .lg\:sl-flex {
+ display: flex;
+ }
+ .lg\:sl-block {
+ display: block;
+ }
+}
+[data-theme="light"] .light\:sl-hidden {
+ display: none;
+}
+[data-theme="dark"] .dark\:sl-hidden {
+ display: none;
+}
+
+/*
+Flip an element around the y-axis when in an RTL context.
+Primarily useful for things where we can’t rely on writing direction like icons.
+
+
+
+In a LTR context: → In a RTL context: ←
+*/
+[dir="rtl"] .rtl\:flip:not(:where([dir="rtl"] [dir="ltr"] *)) {
+ transform: matrix(-1, 0, 0, 1, 0, 0);
+}
+
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Thin.woff2") format("woff2");
+ font-weight: 100;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ThinItalic.woff2") format("woff2");
+ font-weight: 100;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraLight.woff2") format("woff2");
+ font-weight: 200;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraLightItalic.woff2") format("woff2");
+ font-weight: 200;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Light.woff2") format("woff2");
+ font-weight: 300;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-LightItalic.woff2") format("woff2");
+ font-weight: 300;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Regular.woff2") format("woff2");
+ font-weight: 400;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Italic.woff2") format("woff2");
+ font-weight: 400;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Medium.woff2") format("woff2");
+ font-weight: 500;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-MediumItalic.woff2") format("woff2");
+ font-weight: 500;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-SemiBold.woff2") format("woff2");
+ font-weight: 600;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-SemiBoldItalic.woff2") format("woff2");
+ font-weight: 600;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-Bold.woff2") format("woff2");
+ font-weight: 700;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-BoldItalic.woff2") format("woff2");
+ font-weight: 700;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
+ font-weight: 800;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
+ font-weight: 800;
+ font-style: italic;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBold.woff2") format("woff2");
+ font-weight: 900;
+ font-style: normal;
+}
+@font-face {
+ font-family: "Jetbrains Mono";
+ src: url("/fonts/JetBrainsMono-ExtraBoldItalic.woff2") format("woff2");
+ font-weight: 900;
+ font-style: italic;
+}
+
+@font-face {
+ font-family: 'Monocraft';
+ src: url('/fonts/Monocraft.ttf') format('truetype');
+ font-weight: 400;
+ font-style: normal;
+ font-display: swap;
+}
+
+:root {
+ --primary: #54d7a9;
+ --secondary: #9b7eca;
+ --accent: #7ed4fb;
+ --extra: #8fcc75;
+ --text: #ffffff;
+ --alt: #999999;
+ --background: #000000;
+ --font: "Jetbrains Mono", monospace;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ background-color: var(--background);
+ color: var(--text);
+ font-family: var(--font);
+}
+
+body {
+ zoom: 1;
+}
+
+h1,
+h2,
+h3 {
+ font-size: 1rem;
+ font-weight: bold;
+}
+
+a {
+ font-weight: 300;
+}
+a:link,
+a:visited,
+a:active {
+ background-color: transparent;
+ text-decoration: none;
+ color: var(--secondary);
+}
+a:hover {
+ transition-duration: 0.2s;
+ transition-property: color, text-decoration;
+ text-decoration: underline;
+ color: var(--accent);
+}
+
+main {
+ display: block;
+ position: absolute;
+ padding-top: 10%;
+ padding-bottom: 10%;
+ left: 50%;
+ transform: translate(-50%, 0%);
+ background: var(--background);
+ font-weight: 300;
+}
+`