@designsystemsinternational/cloudflare 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/cli/index.ts +196 -0
- package/package.json +32 -0
- package/templates/dist/auth/assets/index-BqXcY2TH.css +1 -0
- package/templates/dist/auth/assets/index-TTeq04ep.js +9 -0
- package/templates/dist/auth/fonts/UniversNextPro-Medium.woff2 +0 -0
- package/templates/dist/auth/fonts/UniversNextPro-Regular.woff2 +0 -0
- package/templates/dist/auth/fonts/UniversNextTypewrtrPro-Rg.woff2 +0 -0
- package/templates/dist/auth/index.html +14 -0
- package/templates/dist/index.html +6 -0
- package/templates/eslint.config.js +23 -0
- package/templates/index.html +12 -0
- package/templates/package-lock.json +5394 -0
- package/templates/package.json +33 -0
- package/templates/postcss.config.cjs +19 -0
- package/templates/public/fonts/UniversNextPro-Medium.woff2 +0 -0
- package/templates/public/fonts/UniversNextPro-Regular.woff2 +0 -0
- package/templates/public/fonts/UniversNextTypewrtrPro-Rg.woff2 +0 -0
- package/templates/src/App.module.css +116 -0
- package/templates/src/App.tsx +116 -0
- package/templates/src/assets/Cloudflare_Logo.svg +51 -0
- package/templates/src/assets/react.svg +1 -0
- package/templates/src/main.tsx +16 -0
- package/templates/src/styles/breakpoints.css +9 -0
- package/templates/src/styles/fonts.css +17 -0
- package/templates/src/styles/globals.css +14 -0
- package/templates/src/styles/reset.css +438 -0
- package/templates/src/styles/textStyles.css +19 -0
- package/templates/src/styles/utilities.css +5 -0
- package/templates/src/styles/variables.css +195 -0
- package/templates/src/vite-env.d.ts +1 -0
- package/templates/tsconfig.app.json +28 -0
- package/templates/tsconfig.json +7 -0
- package/templates/tsconfig.node.json +26 -0
- package/templates/vite.config.ts +15 -0
- package/tsconfig.json +10 -0
- package/worker-configuration.d.ts +10851 -0
- package/workers/password/index.ts +67 -0
- package/workers/password/lib/constants.ts +1 -0
- package/workers/password/lib/schema.ts +8 -0
- package/workers/password/lib/util.ts +28 -0
- package/wrangler.jsonc +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# DSI Cloudflare Package
|
|
2
|
+
|
|
3
|
+
This package provides a set of helpers to deploy both static and dynamic
|
|
4
|
+
websites to Cloudflare Workers. It comes with a nice set of tools:
|
|
5
|
+
|
|
6
|
+
- A CLI to set up the deployment config (including `wrangler.jsonc`) for a repo
|
|
7
|
+
- A Cloudflare Worker to password protect a static website
|
|
8
|
+
- A Vite application that builds the auth templates needed for the password
|
|
9
|
+
worker
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Setting up a new deployment
|
|
14
|
+
|
|
15
|
+
In order to deploy a new site on Cloudflare Worker, run the following:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @designsystemsinternational/cloudflare
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The CLI will guide you through setting up the deployment, including with
|
|
22
|
+
password protection.
|
|
23
|
+
|
|
24
|
+
## Development
|
|
25
|
+
|
|
26
|
+
### File structure
|
|
27
|
+
|
|
28
|
+
The repo is divided into these main folders:
|
|
29
|
+
|
|
30
|
+
- `templates/` holds a Vite app that can be used to make frontend templates. It
|
|
31
|
+
currently just renders the auth templates.
|
|
32
|
+
- `workers/` holds all Cloudflare Workers code. It currently just has a
|
|
33
|
+
`password` worker that renders the auth templates in front of a static site.
|
|
34
|
+
- `cli` holds the code for the npx command
|
|
35
|
+
|
|
36
|
+
### Local development
|
|
37
|
+
|
|
38
|
+
Because of the way Cloudflare workers operate, it's hard to have a single local
|
|
39
|
+
development command to test the entire flow while preserving the Vite hot module
|
|
40
|
+
reloading. So, the development will need to happen in two ways:
|
|
41
|
+
|
|
42
|
+
- To design the auth templates, run `cd templates && npm run dev` to run the
|
|
43
|
+
Vite dev server inside the `/templates` folder. This will enable hot module
|
|
44
|
+
reloading, but the worker auth script won't run, so the actual login process
|
|
45
|
+
won't work.
|
|
46
|
+
- To test the whole flow, run `npm run dev` in the root folder. This runs the
|
|
47
|
+
cloudflare worker in a local environment that matches the production
|
|
48
|
+
environment. With this, you can test the entire login flow, but the auth
|
|
49
|
+
templates will use `vite build` and not run via hot module reloading.
|
|
50
|
+
|
|
51
|
+
All you need is to add a `.env.local` file with the following environment
|
|
52
|
+
variables:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
SECRET=secret
|
|
56
|
+
PASSWORD=yiah
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For testing the CLI, I currently just create a new test package alongside this
|
|
60
|
+
folder and run `npx ../cloudflare`.
|
package/cli/index.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { confirm, input, select } from "@inquirer/prompts";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { exec } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base setup
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
const hasWrangler = existsSync("wrangler.jsonc");
|
|
14
|
+
const packageJson = JSON.parse(readFileSync("./package.json", "utf-8"));
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Prompts
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const isStaticSite = await confirm({
|
|
21
|
+
message: "This CLI currently only supports static sites. Continue?",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!isStaticSite) {
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const name = await input({
|
|
29
|
+
message: "Name of the Cloudflare Workers site",
|
|
30
|
+
default: packageJson?.name ?? "my-site",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const buildCmd = await input({
|
|
34
|
+
message: "Build command",
|
|
35
|
+
default: "npm run build",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const buildDir = await input({
|
|
39
|
+
message: "Build directory",
|
|
40
|
+
default: "./dist",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const notFoundHandling = await select({
|
|
44
|
+
message: "Not found handling",
|
|
45
|
+
choices: [
|
|
46
|
+
{
|
|
47
|
+
name: "Single Page Application",
|
|
48
|
+
value: "single-page-application",
|
|
49
|
+
description: "Redirect to the index page when the asset is not found",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "404 Page",
|
|
53
|
+
value: "404-page",
|
|
54
|
+
description: "Render a 404 page when the asset is not found",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "None",
|
|
58
|
+
value: "none",
|
|
59
|
+
description: "Return a 404 status code when the asset is not found",
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const hasCustomDomain = await confirm({
|
|
65
|
+
message: "Add a custom domain?",
|
|
66
|
+
default: false,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
let customDomain;
|
|
70
|
+
if (hasCustomDomain) {
|
|
71
|
+
customDomain = await input({
|
|
72
|
+
message: "Custom domain",
|
|
73
|
+
default: "xxx.ionaldesignsystemsinternat.com",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const passwordProtection = await confirm({
|
|
78
|
+
message: "Enable password protection?",
|
|
79
|
+
default: false,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create wrangler.jsonc
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
const json: any = {
|
|
87
|
+
name,
|
|
88
|
+
compatibility_date: new Date().toISOString().split("T")[0],
|
|
89
|
+
assets: {
|
|
90
|
+
directory: buildDir,
|
|
91
|
+
not_found_handling: notFoundHandling,
|
|
92
|
+
binding: "ASSETS",
|
|
93
|
+
},
|
|
94
|
+
keep_vars: true,
|
|
95
|
+
workers_dev: true,
|
|
96
|
+
observability: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (hasCustomDomain && customDomain) {
|
|
102
|
+
json.routes = [
|
|
103
|
+
{
|
|
104
|
+
pattern: customDomain,
|
|
105
|
+
custom_domain: true,
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (passwordProtection) {
|
|
111
|
+
json.main =
|
|
112
|
+
"./node_modules/@designsystemsinternational/cloudflare/workers/password/index.ts";
|
|
113
|
+
json.assets.run_worker_first = true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Write
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
const wranglerString = JSON.stringify(json, null, 2);
|
|
121
|
+
const buildCmdSplit = buildCmd.split(" ");
|
|
122
|
+
const buildCmdName = buildCmdSplit[2];
|
|
123
|
+
const shouldWriteBuildCmd =
|
|
124
|
+
buildCmdSplit[0] === "npm" &&
|
|
125
|
+
buildCmdSplit[1] === "run" &&
|
|
126
|
+
packageJson?.scripts?.[buildCmdName];
|
|
127
|
+
const copyCmd =
|
|
128
|
+
"cp -r node_modules/@designsystemsinternational/cloudflare/templates/dist/auth dist/auth/";
|
|
129
|
+
|
|
130
|
+
console.log("");
|
|
131
|
+
console.log(
|
|
132
|
+
hasWrangler
|
|
133
|
+
? "The existing wrangler.jsonc file will be overwritten with this content:"
|
|
134
|
+
: "A wrangler.jsonc file will be created with this content:"
|
|
135
|
+
);
|
|
136
|
+
console.log(wranglerString);
|
|
137
|
+
console.log("");
|
|
138
|
+
if (shouldWriteBuildCmd) {
|
|
139
|
+
console.log("The following command will be added to the build command:");
|
|
140
|
+
console.log(copyCmd);
|
|
141
|
+
console.log("");
|
|
142
|
+
}
|
|
143
|
+
if (passwordProtection) {
|
|
144
|
+
console.log(
|
|
145
|
+
"The @designsystemsinternational/cloudflare package will be installed automatically."
|
|
146
|
+
);
|
|
147
|
+
console.log("");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const confirmWrite = await confirm({
|
|
151
|
+
message: "Continue?",
|
|
152
|
+
});
|
|
153
|
+
if (!confirmWrite) {
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
writeFileSync("./wrangler.jsonc", wranglerString, "utf-8");
|
|
158
|
+
|
|
159
|
+
if (shouldWriteBuildCmd) {
|
|
160
|
+
packageJson.scripts[buildCmdName] =
|
|
161
|
+
packageJson.scripts[buildCmdName] + " && " + copyCmd;
|
|
162
|
+
writeFileSync(
|
|
163
|
+
"./package.json",
|
|
164
|
+
JSON.stringify(packageJson, null, 2),
|
|
165
|
+
"utf-8"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (passwordProtection) {
|
|
170
|
+
console.log("Installing @designsystemsinternational/cloudflare...");
|
|
171
|
+
try {
|
|
172
|
+
await execAsync("npm install @designsystemsinternational/cloudflare");
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(
|
|
175
|
+
"Failed to install @designsystemsinternational/cloudflare package. Please install yourself."
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`Project setup complete!
|
|
181
|
+
|
|
182
|
+
Next steps:
|
|
183
|
+
- Create a new Cloudflare Workers site with this name: ${name}
|
|
184
|
+
- Add this build command to the site config: ${buildCmd}
|
|
185
|
+
`);
|
|
186
|
+
|
|
187
|
+
if (passwordProtection) {
|
|
188
|
+
console.log(`- Add the following environment variables under Settings > Variables and Secrets
|
|
189
|
+
- SECRET: A random string to encrypt the password
|
|
190
|
+
- PASSWORD: The password you want to use`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!shouldWriteBuildCmd) {
|
|
194
|
+
console.log(`- Add the following command to the build command in package.json:
|
|
195
|
+
${copyCmd}`);
|
|
196
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@designsystemsinternational/cloudflare",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "git+https://github.com/designsystemsinternational/cloudflare.git"
|
|
6
|
+
},
|
|
7
|
+
"version": "0.1.0",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"cloudflare": "cli/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev": "cd templates && npm run build && cd .. && wrangler dev",
|
|
14
|
+
"build": "tsc -b && vite build",
|
|
15
|
+
"lint": "eslint .",
|
|
16
|
+
"deploy": "cd templates && npm run build && cd .. && wrangler deploy",
|
|
17
|
+
"types": "wrangler types"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@tsndr/cloudflare-worker-jwt": "^3.2.0",
|
|
21
|
+
"worktop": "^0.7.3",
|
|
22
|
+
"zod": "^4.1.13"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@inquirer/prompts": "^8.1.0",
|
|
26
|
+
"@types/node": "^24.10.1",
|
|
27
|
+
"eslint": "^9.33.0",
|
|
28
|
+
"typescript": "~5.8.3",
|
|
29
|
+
"typescript-eslint": "^8.39.1",
|
|
30
|
+
"wrangler": "^4.53.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
._root_biwuk_1{display:grid;grid-template-columns:repeat(3,1fr);justify-content:center;align-items:end;min-height:100vh;width:100vw;padding-bottom:var(--spacing-layout-gutter)}@media(min-width:56rem){._root_biwuk_1{align-items:center}}._logo_biwuk_17{position:absolute;top:0;left:0;right:0;display:grid;grid-template-columns:repeat(3,1fr);font-size:var(--text-sm);font-weight:var(--font-weight-medium)}._logo_biwuk_17>div{padding-top:var(--spacing-layout-gutter);padding-right:var(--spacing-layout-gutter);padding-left:var(--spacing-layout-gutter)}._logo_biwuk_17>div:last-child{text-align:right}._container_biwuk_38{padding-right:var(--spacing-layout-gutter);padding-left:var(--spacing-layout-gutter);grid-column:1 / -1}@media(min-width:56rem){._container_biwuk_38{grid-column:2 / 3}}._box_biwuk_50{--input-padding: calc(1.2 * var(--spacing-sm)) var(--spacing-sm) var(--spacing-sm);display:flex;flex-direction:column;justify-content:space-between;gap:var(--spacing-xl);padding:var(--spacing-sm);background-color:var(--color-overlay);border-radius:var(--radius-sm);-webkit-backdrop-filter:var(--backdrop-filter);backdrop-filter:var(--backdrop-filter)}._box_biwuk_50 form{display:flex;flex-direction:column;gap:var(--spacing-2xs)}._box_biwuk_50 [aria-busy]{opacity:.5;cursor:wait}._box_biwuk_50 input{border:1px solid var(--color-border-default);padding:var(--input-padding);font-size:var(--text-sm);border-radius:var(--radius-xs);outline:none;font-family:var(--font-mono)}._box_biwuk_50 input:not([aria-busy]):focus{border-color:var(--color-border-focus)}._box_biwuk_50 button{background:var(--color-bg-inverted-primary);color:var(--color-text-inverted-primary);font-size:var(--text-sm);padding:var(--input-padding);border-radius:var(--radius-xs)}._box_biwuk_50 button:not([aria-busy]):hover{background:var(--color-bg-inverted-secondary);cursor:pointer}._errorBox_biwuk_99[aria-hidden]{visibility:hidden}._errorBox_biwuk_99{padding:var(--spacing-sm);background:var(--color-bg-inverted-tertiary);line-height:var(--leading-loose);border-radius:var(--radius-xs)}._errorBox_biwuk_99 a{font-weight:var(--font-weight-medium)}._errorBox_biwuk_99 a:hover{-webkit-text-decoration:underline;text-decoration:underline}@font-face{font-family:Univers;font-weight:400;src:url(/auth/fonts/UniversNextPro-Regular.woff2) format("woff2")}@font-face{font-family:Univers;font-weight:600;src:url(/auth/fonts/UniversNextPro-Medium.woff2) format("woff2")}@font-face{font-family:Typewriter;font-weight:400;src:url(/auth/fonts/UniversNextTypewrtrPro-Rg.woff2) format("woff2")}@layer reset{*,:after,:before,::backdrop,::file-selector-button{box-sizing:border-box;margin:0;padding:0;border:0 solid}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:--theme(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:--theme(--default-font-feature-settings,normal);font-variation-settings:--theme(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;-webkit-text-decoration:underline;text-decoration:underline}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:--theme(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:--theme(--default-mono-font-feature-settings,normal);font-variation-settings:--theme(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea,::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;border-radius:0;background-color:transparent;opacity:1}:where(select[multiple]) optgroup{font-weight:bolder}:where(select[size]) optgroup{font-weight:bolder}:where(select[multiple]) optgroup option{padding-left:20px}:where(select[size]) optgroup option{padding-left:20px}::file-selector-button{margin-right:4px}::-moz-placeholder{opacity:1}::placeholder{opacity:1}@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px){::-moz-placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field{padding-top:0;padding-bottom:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]),::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}:root{--base-color-black: 0, 0, 0;--base-color-white: 255, 255, 255;--base-color-grey-100: 240, 240, 240;--base-color-grey-300: 204, 204, 204;--base-color-grey-500: 136, 136, 136;--base-color-grey-800: 35, 35, 35;--base-color-grey-900: 15, 15, 15;--color-text-primary: rgb(var(--base-color-black));--color-text-secondary: rgb(var(--base-color-grey-500));--color-text-tertiary: rgb(var(--base-color-grey-300));--color-text-inverted-primary: rgb(var(--base-color-white));--color-text-inverted-secondary: rgba(var(--base-color-white), 50%);--color-bg-primary: rgb(var(--base-color-white));--color-bg-secondary: rgb(var(--base-color-grey-100));--color-bg-inverted-primary: rgb(var(--base-color-black));--color-bg-inverted-secondary: rgb(var(--base-color-grey-800));--color-bg-inverted-tertiary: rgba(var(--base-color-black), 7.5%);--color-overlay: rgba(var(--base-color-white), 40%);--color-overlay-inverted: rgba(var(--base-color-black), 80%);--color-border-default: rgba(var(--base-color-black), 10%);--color-border-focus: rgb(var(--base-color-grey-800));--spacing-4xs: clamp(2px, 1.3077px + .1923vw, 4px);--spacing-3xs: clamp(4px, 3.3077px + .1923vw, 6px);--spacing-2xs: clamp(6px, 5.3077px + .1923vw, 8px);--spacing-xs: clamp(8px, 6.6154px + .3846vw, 12px);--spacing-sm: clamp(12px, 10.6154px + .3846vw, 16px);--spacing-md: clamp(16px, 13.2308px + .7692vw, 24px);--spacing-lg: clamp(24px, 21.2308px + .7692vw, 32px);--spacing-xl: clamp(32px, 26.4615px + 1.5385vw, 48px);--spacing-2xl: clamp(48px, 42.4615px + 1.5385vw, 64px);--spacing-3xl: clamp(64px, 52.9231px + 3.0769vw, 96px);--spacing-4xl: clamp(80px, 57.8462px + 6.1538vw, 144px);--spacing-5xl: clamp(144px, 117.6923px + 7.3077vw, 220px);--spacing-layout-gutter: clamp(8px, 7.3077px + .1923vw, 10px);--spacing-grid-gap: calc(2 * var(--spacing-layout-gutter));--text-xs: clamp(13px, 12.6538px + .0962vw, 14px);--text-sm: clamp(14px, 12.9615px + .2885vw, 17px);--leading-none: 1;--leading-tight: 1.05;--leading-normal: 1.25;--leading-loose: 1.5;--tracking-widest: .05em;--tracking-wider: .025em;--tracking-wide: .001em;--tracking-none: 0;--tracking-tight: -.01em;--tracking-tighter: -.02em;--tracking-tightest: -.04em;--font-sans: "Univers", sans-serif;--font-mono: "Typewriter", monospace;--measure-xs: 30ch;--measure-s: 40ch;--measure-m: 55ch;--measure-l: 75ch;--measure-xl: 80ch;--measure-2xl: 100ch;--font-weight-regular: 400;--font-weight-medium: 600;--ease-in-out: cubic-bezier(.4, 0, .2, 1);--duration-fast: .15s;--duration-slow: .4s;--aspect-square: 1 / 1;--aspect-wide: 16 / 9;--radius-xs: 1px;--radius-sm: 2px;--radius-full: 9999px;--backdrop-filter: blur(10px);--z-index-tooltip-hover-target: 90;--z-index-tooltip: 100;--z-index-control-elements: 200}@media(prefers-color-scheme:dark){:root{--color-text-primary: rgb(var(--base-color-white));--color-text-secondary: rgba(var(--base-color-white), 50%);--color-text-inverted-primary: rgb(var(--base-color-black));--color-text-inverted-secondary: rgba(var(--base-color-black), 50%);--color-bg-primary: rgb(var(--base-color-black));--color-bg-secondary: rgb(var(--base-color-grey-900));--color-bg-inverted-primary: rgb(var(--base-color-white));--color-bg-inverted-secondary: rgb(var(--base-color-grey-100));--color-bg-inverted-tertiary: rgba(var(--base-color-white), 20%);--color-overlay: rgba(var(--base-color-white), 8%);--color-overlay-inverted: rgba(var(--base-color-white), 80%);--color-border-default: rgba(var(--base-color-white), 20%);--color-border-focus: rgba(var(--base-color-white), 70%)}}body{padding:0;overflow-x:hidden;background:var(--color-bg-secondary);color:var(--color-text-primary);font-family:var(--font-sans);font-size:var(--text-md);line-height:var(--leading-normal);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}:where(.text-xs){font-size:var(--text-xs);letter-spacing:var(--tracking-wider);line-height:var(--leading-normal)}:where(.text-sm){font-size:var(--text-sm);letter-spacing:var(--tracking-wide);line-height:var(--leading-normal)}:where(.text-primary){color:var(--color-text-primary)}:where(.text-secondary){color:var(--color-text-secondary)}
|