From 2c388aeef4b762c75f6b15d3c410bae9a49dbf17 Mon Sep 17 00:00:00 2001
From: Samir
Date: Sat, 22 Aug 2026 08:53:03 -0400
Subject: [PATCH 1/6] Restore compact component gallery
Prompt: Keep component specimens in a compact multi-column layout, let masonry absorb vertical gaps, distinguish category and type filters, and use neutral gray for the non-primary nav button.
---
.../src/styles/40-inline.css | 44 ++++++++++++-------
1 file changed, 28 insertions(+), 16 deletions(-)
diff --git a/artifacts/structured-liquidity/src/styles/40-inline.css b/artifacts/structured-liquidity/src/styles/40-inline.css
index 7d22855..b99992f 100644
--- a/artifacts/structured-liquidity/src/styles/40-inline.css
+++ b/artifacts/structured-liquidity/src/styles/40-inline.css
@@ -1424,7 +1424,7 @@ body {
}
/* ============================================================
- PRECISION PASS — one editorial grid, full-height specimens,
+ PRECISION PASS — one editorial grid, compact specimens,
higher-fidelity liquid state, and legible accent interaction.
============================================================ */
.nav-cluster,
@@ -1452,7 +1452,7 @@ body {
min-height: 100%;
padding: 0.58rem 0.72rem;
border-width: 1px;
- background: var(--bg-2);
+ background: color-mix(in srgb, var(--ink) 10%, var(--bg-2));
box-shadow: none;
color: var(--ink);
font-size: 0.74rem;
@@ -1498,20 +1498,19 @@ body {
.tenet ul { margin-top: 0; }
#components .kit-grid.gallery {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- align-items: stretch;
- gap: 1px;
- column-count: auto;
+ display: block;
+ column-count: 2;
+ column-gap: 1px;
}
#components .kit-grid.gallery .kit-cell {
display: flex;
flex-direction: column;
- width: auto;
- height: 100%;
+ width: 100%;
+ height: auto;
min-width: 0;
- margin: 0;
- break-inside: auto;
+ margin: 0 0 1px;
+ break-inside: avoid;
+ -webkit-column-break-inside: avoid;
}
.sl-button-specimen {
flex-direction: column;
@@ -1519,18 +1518,31 @@ body {
justify-content: flex-start;
flex-wrap: nowrap;
}
-.kit-filter.active {
+.kit-filters[data-axis="cat"] .kit-filter {
+ background: color-mix(in srgb, var(--ink) 10%, var(--bg-2));
+ border-color: color-mix(in srgb, var(--ink) 58%, transparent);
+ box-shadow: 2px 2px 0 0 var(--hard-shadow);
+}
+.kit-filters[data-axis="cat"] .kit-filter:hover {
+ background: color-mix(in srgb, var(--ink) 15%, var(--bg-2));
+ box-shadow: 2px 2px 0 0 var(--hard-shadow);
+}
+.kit-filters[data-axis="cat"] .kit-filter.active {
background: var(--accent);
color: var(--accent-ink);
border-color: var(--accent);
}
-.kit-filter.active:hover { color: var(--accent-ink); }
-[data-mode="dark"] .kit-filter.active {
+.kit-filters[data-axis="cat"] .kit-filter.active:hover { color: var(--accent-ink); }
+.kit-filters[data-axis="kind"] .kit-filter {
+ background: color-mix(in srgb, var(--ink) 4%, transparent);
+ border-color: color-mix(in srgb, var(--ink) 34%, transparent);
+}
+[data-mode="dark"] .kit-filters[data-axis="cat"] .kit-filter.active {
background: color-mix(in srgb, var(--accent) 54%, var(--bg-2));
color: #fff;
border-color: color-mix(in srgb, var(--accent) 76%, white);
}
-[data-mode="dark"] .kit-filter.active:hover { color: #fff; }
+[data-mode="dark"] .kit-filters[data-axis="cat"] .kit-filter.active:hover { color: #fff; }
[data-mode="dark"] .kit-filters[data-axis="kind"] .kit-filter.active {
background: color-mix(in srgb, var(--accent) 32%, transparent);
color: #fff;
@@ -1555,7 +1567,7 @@ body {
.nav .nav-utility { padding-inline: 0.58rem; }
}
@media (max-width: 600px) {
- #components .kit-grid.gallery { grid-template-columns: 1fr; }
+ #components .kit-grid.gallery { column-count: 1; }
.nav .nav-utility { font-size: 0; gap: 0; }
.tenet {
display: flex;
From 9407dd218aef30f03e04de2027f8652ddcb386d8 Mon Sep 17 00:00:00 2001
From: Samir
Date: Sat, 22 Aug 2026 09:36:34 -0400
Subject: [PATCH 2/6] Protect Structured staging with Clerk
Prompt: Can you have staging on the live domain? You can add clerk auth for access and make sure samir@interspace.ventures has access. Should be staging.structured.glass.
---
artifacts/api-server/package.json | 1 +
artifacts/api-server/src/app.ts | 21 +-
.../src/middleware/staging-access.ts | 218 ++++++++++++++++++
artifacts/structured-liquidity/src/App.tsx | 12 +
artifacts/structured-liquidity/src/index.css | 18 ++
pnpm-lock.yaml | 86 +++++++
6 files changed, 348 insertions(+), 8 deletions(-)
create mode 100644 artifacts/api-server/src/middleware/staging-access.ts
diff --git a/artifacts/api-server/package.json b/artifacts/api-server/package.json
index 6916f27..6f26481 100644
--- a/artifacts/api-server/package.json
+++ b/artifacts/api-server/package.json
@@ -10,6 +10,7 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
+ "@clerk/express": "^2.1.61",
"@workspace/api-zod": "workspace:*",
"@workspace/db": "workspace:*",
"cookie-parser": "^1.4.7",
diff --git a/artifacts/api-server/src/app.ts b/artifacts/api-server/src/app.ts
index 0d60a0d..6375d64 100644
--- a/artifacts/api-server/src/app.ts
+++ b/artifacts/api-server/src/app.ts
@@ -4,6 +4,7 @@ import cors from "cors";
import pinoHttp from "pino-http";
import router from "./routes";
import { logger } from "./lib/logger";
+import { installStagingAccess } from "./middleware/staging-access";
const app: Express = express();
@@ -30,18 +31,22 @@ app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
+installStagingAccess(app);
+
app.use("/api", router);
// Railway runs the API and web client as a single service. This keeps relative
// /api requests on the canonical domain when the built Vite client is served.
-const clientDist = process.env["CLIENT_DIST"] ?? path.resolve(
- import.meta.dirname,
- "..",
- "..",
- "structured-liquidity",
- "dist",
- "public",
-);
+const clientDist =
+ process.env["CLIENT_DIST"] ??
+ path.resolve(
+ import.meta.dirname,
+ "..",
+ "..",
+ "structured-liquidity",
+ "dist",
+ "public",
+ );
app.use(express.static(clientDist));
app.use((req, res, next) => {
diff --git a/artifacts/api-server/src/middleware/staging-access.ts b/artifacts/api-server/src/middleware/staging-access.ts
new file mode 100644
index 0000000..f43dc62
--- /dev/null
+++ b/artifacts/api-server/src/middleware/staging-access.ts
@@ -0,0 +1,218 @@
+import { clerkClient, clerkMiddleware, getAuth } from "@clerk/express";
+import type { Express, NextFunction, Request, Response } from "express";
+
+const STAGING_ROBOTS = "noindex, nofollow, noarchive, nosnippet";
+const AUTHORIZATION_CACHE_TTL_MS = 60_000;
+
+type StagingAccessConfig = {
+ allowedEmails: Set;
+ authorizedParties: string[];
+ canonicalOrigin: string;
+ publishableKey: string;
+ secretKey: string;
+ signInUrl: string;
+};
+
+type CachedAuthorization = {
+ allowed: boolean;
+ expiresAt: number;
+};
+
+const authorizationCache = new Map();
+
+function parseCsv(value: string | undefined): string[] {
+ return (value ?? "")
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean);
+}
+
+function parseHttpsUrl(value: string, name: string): string {
+ let url: URL;
+
+ try {
+ url = new URL(value);
+ } catch {
+ throw new Error(`${name} must be an absolute URL`);
+ }
+
+ if (url.protocol !== "https:") {
+ throw new Error(`${name} must use https`);
+ }
+
+ return (
+ url.origin + (url.pathname === "/" ? "" : url.pathname.replace(/\/$/, ""))
+ );
+}
+
+function readConfig(): StagingAccessConfig {
+ const publishableKey = process.env["CLERK_PUBLISHABLE_KEY"]?.trim();
+ const secretKey = process.env["CLERK_SECRET_KEY"]?.trim();
+ const signInUrlValue = process.env["CLERK_SIGN_IN_URL"]?.trim();
+ const canonicalOriginValue = process.env["STAGING_CANONICAL_ORIGIN"]?.trim();
+ const allowedEmails = new Set(
+ parseCsv(process.env["STAGING_ALLOWED_EMAILS"]).map((email) =>
+ email.toLowerCase(),
+ ),
+ );
+ const authorizedParties = parseCsv(
+ process.env["STAGING_AUTHORIZED_PARTIES"],
+ ).map((party) => parseHttpsUrl(party, "STAGING_AUTHORIZED_PARTIES"));
+
+ if (
+ !publishableKey ||
+ !secretKey ||
+ !signInUrlValue ||
+ !canonicalOriginValue
+ ) {
+ throw new Error(
+ "Staging auth requires CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, CLERK_SIGN_IN_URL, and STAGING_CANONICAL_ORIGIN",
+ );
+ }
+
+ if (allowedEmails.size === 0 || authorizedParties.length === 0) {
+ throw new Error(
+ "Staging auth requires non-empty STAGING_ALLOWED_EMAILS and STAGING_AUTHORIZED_PARTIES",
+ );
+ }
+
+ const canonicalOrigin = parseHttpsUrl(
+ canonicalOriginValue,
+ "STAGING_CANONICAL_ORIGIN",
+ );
+ const signInUrl = parseHttpsUrl(signInUrlValue, "CLERK_SIGN_IN_URL");
+
+ if (!authorizedParties.includes(canonicalOrigin)) {
+ throw new Error(
+ "STAGING_AUTHORIZED_PARTIES must include STAGING_CANONICAL_ORIGIN exactly",
+ );
+ }
+
+ return {
+ allowedEmails,
+ authorizedParties,
+ canonicalOrigin,
+ publishableKey,
+ secretKey,
+ signInUrl,
+ };
+}
+
+function isHealthRequest(req: Request): boolean {
+ return req.path === "/api/health" || req.path === "/api/healthz";
+}
+
+function isApiRequest(req: Request): boolean {
+ return req.path === "/api" || req.path.startsWith("/api/");
+}
+
+function signInRedirect(config: StagingAccessConfig, req: Request): string {
+ const signInUrl = new URL(config.signInUrl);
+ const returnUrl = new URL(req.originalUrl || "/", config.canonicalOrigin);
+ signInUrl.searchParams.set("redirect_url", returnUrl.toString());
+ return signInUrl.toString();
+}
+
+async function isAllowedUser(
+ userId: string,
+ allowedEmails: Set,
+): Promise {
+ const cached = authorizationCache.get(userId);
+ const now = Date.now();
+
+ if (cached && cached.expiresAt > now) {
+ return cached.allowed;
+ }
+
+ const user = await clerkClient.users.getUser(userId);
+ const allowed = user.emailAddresses.some((email) =>
+ allowedEmails.has(email.emailAddress.toLowerCase()),
+ );
+
+ authorizationCache.set(userId, {
+ allowed,
+ expiresAt: now + AUTHORIZATION_CACHE_TTL_MS,
+ });
+
+ return allowed;
+}
+
+export function installStagingAccess(app: Express): void {
+ if (process.env["STAGING_AUTH_REQUIRED"] !== "true") return;
+
+ const config = readConfig();
+ const canonicalHostname = new URL(config.canonicalOrigin).hostname;
+
+ app.use((req, res, next) => {
+ res.setHeader("X-Robots-Tag", STAGING_ROBOTS);
+ res.setHeader("Referrer-Policy", "same-origin");
+
+ if (!isHealthRequest(req)) {
+ res.setHeader("Cache-Control", "private, no-store");
+ }
+
+ next();
+ });
+
+ app.get("/robots.txt", (_req, res) => {
+ res.type("text/plain").send("User-agent: *\nDisallow: /\n");
+ });
+
+ app.use((req, res, next) => {
+ if (
+ !isHealthRequest(req) &&
+ !isApiRequest(req) &&
+ req.hostname !== canonicalHostname
+ ) {
+ const destination = new URL(
+ req.originalUrl || "/",
+ config.canonicalOrigin,
+ );
+ res.redirect(308, destination.toString());
+ return;
+ }
+
+ next();
+ });
+
+ app.use(
+ clerkMiddleware({
+ authorizedParties: config.authorizedParties,
+ publishableKey: config.publishableKey,
+ secretKey: config.secretKey,
+ signInUrl: config.signInUrl,
+ }),
+ );
+
+ app.use(async (req: Request, res: Response, next: NextFunction) => {
+ if (isHealthRequest(req)) {
+ next();
+ return;
+ }
+
+ const { userId } = getAuth(req);
+
+ if (!userId) {
+ if (isApiRequest(req)) {
+ res.status(401).json({ error: "Authentication required" });
+ return;
+ }
+
+ res.redirect(302, signInRedirect(config, req));
+ return;
+ }
+
+ try {
+ if (!(await isAllowedUser(userId, config.allowedEmails))) {
+ res
+ .status(403)
+ .send("This account does not have access to Structured staging.");
+ return;
+ }
+
+ next();
+ } catch (error) {
+ next(error);
+ }
+ });
+}
diff --git a/artifacts/structured-liquidity/src/App.tsx b/artifacts/structured-liquidity/src/App.tsx
index ba2f205..2a283d7 100644
--- a/artifacts/structured-liquidity/src/App.tsx
+++ b/artifacts/structured-liquidity/src/App.tsx
@@ -13,12 +13,24 @@ import { Footer } from "@/components/site/Footer";
import { Tweaker } from "@/components/site/Tweaker";
export function App() {
+ const isStaging =
+ window.location.hostname === "staging.structured.glass" ||
+ import.meta.env.VITE_DEPLOYMENT_ENVIRONMENT === "staging";
+
useEffect(() => {
initBehaviors();
}, []);
return (
+ {isStaging ? (
+
+ Staging
+
+ ) : null}
diff --git a/artifacts/structured-liquidity/src/index.css b/artifacts/structured-liquidity/src/index.css
index deab317..407c8ac 100644
--- a/artifacts/structured-liquidity/src/index.css
+++ b/artifacts/structured-liquidity/src/index.css
@@ -30,3 +30,21 @@
--font-body: var(--body);
--font-mono: var(--mono);
}
+
+.staging-environment-label {
+ position: fixed;
+ z-index: 10000;
+ right: 0.75rem;
+ bottom: 0.75rem;
+ padding: 0.45rem 0.65rem;
+ border: var(--border-w) solid rgb(var(--edge));
+ background: var(--accent);
+ color: var(--accent-ink);
+ box-shadow: 3px 3px 0 0 var(--hard-shadow);
+ font-family: var(--mono);
+ font-size: 0.6875rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ line-height: 1;
+ text-transform: uppercase;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3e35854..48691ad 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -80,6 +80,9 @@ importers:
artifacts/api-server:
dependencies:
+ '@clerk/express':
+ specifier: ^2.1.61
+ version: 2.1.61(express@5.2.1)
'@workspace/api-zod':
specifier: workspace:*
version: link:../../lib/api-zod
@@ -640,6 +643,28 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
+ '@clerk/backend@3.16.10':
+ resolution: {integrity: sha512-rYGgr3sshL6Jaq5bvw3hGYnvOj31o6Yie/jWYiNUnlET9RFH82GywxWIGv6YVGSdYIFVm6DnaCyKqXc/hff/hA==}
+ engines: {node: '>=20.9.0'}
+
+ '@clerk/express@2.1.61':
+ resolution: {integrity: sha512-8I+VvIfobwWmV3fDttDJsZyw1xElh9EyojxKvchjZ+V/aXroNxPYr3JlX/6VqK4uf0Nb8/yyYSevzo5qH0ar6g==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ express: ^4.17.0 || ^5.0.0
+
+ '@clerk/shared@4.29.3':
+ resolution: {integrity: sha512-sc9CNFDR3q+tJ8N5MkGcHGiXlgupQ6DMcZlXdk88kfo1Pl7G9EFK8e4mzp5D9OiYGRJ7rPnZCKiWoyInD59QXw==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
+ react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
'@commander-js/extra-typings@14.0.0':
resolution: {integrity: sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg==}
peerDependencies:
@@ -2191,6 +2216,9 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
+ '@stablelib/base64@1.0.1':
+ resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
+
'@tabby_ai/hijri-converter@1.0.5':
resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==}
engines: {node: '>=16.0.0'}
@@ -2632,6 +2660,10 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -2874,6 +2906,9 @@ packages:
fast-safe-stringify@2.1.1:
resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==}
+ fast-sha256@1.3.0:
+ resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
+
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
@@ -2970,6 +3005,9 @@ packages:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
+ glob-to-regexp@0.4.1:
+ resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
+
globby@16.1.0:
resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==}
engines: {node: '>=20'}
@@ -3067,6 +3105,10 @@ packages:
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
engines: {node: '>=10'}
+ js-cookie@3.0.7:
+ resolution: {integrity: sha512-z/wZZgDrkNV1eA0ULjM/F9/50Ya8fbzgKneSpoPsXSGd0KnpdtHfOZWK+GcwLk+EZbS4F9RBhU+K2RgzuDaItw==}
+ engines: {node: '>=20'}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3688,6 +3730,9 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
+ standardwebhooks@1.0.0:
+ resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
+
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
@@ -4052,6 +4097,32 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
+ '@clerk/backend@3.16.10':
+ dependencies:
+ '@clerk/shared': 4.29.3
+ standardwebhooks: 1.0.0
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - react
+ - react-dom
+
+ '@clerk/express@2.1.61(express@5.2.1)':
+ dependencies:
+ '@clerk/backend': 3.16.10
+ '@clerk/shared': 4.29.3
+ express: 5.2.1
+ tslib: 2.8.1
+ transitivePeerDependencies:
+ - react
+ - react-dom
+
+ '@clerk/shared@4.29.3':
+ dependencies:
+ '@tanstack/query-core': 5.100.9
+ dequal: 2.0.3
+ glob-to-regexp: 0.4.1
+ js-cookie: 3.0.7
+
'@commander-js/extra-typings@14.0.0(commander@14.0.3)':
dependencies:
commander: 14.0.3
@@ -5386,6 +5457,8 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
+ '@stablelib/base64@1.0.1': {}
+
'@tabby_ai/hijri-converter@1.0.5': {}
'@tailwindcss/node@4.3.0':
@@ -5798,6 +5871,8 @@ snapshots:
depd@2.0.0: {}
+ dequal@2.0.3: {}
+
detect-libc@2.1.2: {}
detect-node-es@1.1.0: {}
@@ -6066,6 +6141,8 @@ snapshots:
fast-safe-stringify@2.1.1: {}
+ fast-sha256@1.3.0: {}
+
fast-uri@3.1.2: {}
fastq@1.20.1:
@@ -6161,6 +6238,8 @@ snapshots:
dependencies:
is-glob: 4.0.3
+ glob-to-regexp@0.4.1: {}
+
globby@16.1.0:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
@@ -6233,6 +6312,8 @@ snapshots:
joycon@3.1.1: {}
+ js-cookie@3.0.7: {}
+
js-tokens@4.0.0: {}
js-yaml@4.1.1:
@@ -6871,6 +6952,11 @@ snapshots:
split2@4.2.0: {}
+ standardwebhooks@1.0.0:
+ dependencies:
+ '@stablelib/base64': 1.0.1
+ fast-sha256: 1.3.0
+
statuses@2.0.2: {}
string-argv@0.3.2: {}
From 9c936acf11187e071481312d4d92e2c8ec1124da Mon Sep 17 00:00:00 2001
From: Samir
Date: Sat, 22 Aug 2026 22:02:31 -0400
Subject: [PATCH 3/6] Audit and refine mobile component gallery
Prompt: Audit the mobile design, replace icon-only navigation with a clear hamburger menu, left-align mobile hierarchy, fix broken narrow-width components, remove ghost and link from the button specimen, replace real-app placeholder copy with a coherent fictional science-fiction theme, and advise whether Structured should be surfaced through Exobase.
---
.../public/r/ai-chat.json | 2 +-
.../public/r/media-player.json | 2 +-
.../public/r/mobile-nav.json | 2 +-
.../public/r/site-templates.json | 2 +-
.../src/components/site/Components.tsx | 4 +-
.../src/components/site/Nav.tsx | 73 +++-
.../src/components/site/Templates.tsx | 8 +-
.../src/components/site/kit/KitActions.tsx | 78 ++--
.../components/site/kit/KitContentData.tsx | 16 +-
.../components/site/kit/KitDataDisplay.tsx | 47 ++-
.../components/site/kit/KitFieldsControls.tsx | 70 +++-
.../components/site/kit/KitFormsInputs.tsx | 10 +-
.../site/kit/KitLayoutStructure.tsx | 26 +-
.../components/site/kit/KitMenusOverlays.tsx | 82 ++++-
.../components/site/kit/KitNavDisclosure.tsx | 4 +-
.../src/components/site/kit/KitPatterns.tsx | 48 +--
.../components/templates/site-templates.tsx | 9 +-
.../src/components/ui/ai-chat.tsx | 14 +-
.../src/components/ui/media-player.tsx | 4 +-
.../src/components/ui/mobile-nav.tsx | 34 +-
.../src/styles/40-inline.css | 338 ++++++++++++++++++
21 files changed, 707 insertions(+), 166 deletions(-)
diff --git a/artifacts/structured-liquidity/public/r/ai-chat.json b/artifacts/structured-liquidity/public/r/ai-chat.json
index 6fed79f..56ed391 100644
--- a/artifacts/structured-liquidity/public/r/ai-chat.json
+++ b/artifacts/structured-liquidity/public/r/ai-chat.json
@@ -14,7 +14,7 @@
"files": [
{
"path": "registry/ui/ai-chat.tsx",
- "content": "import * as React from \"react\";\nimport { Sparkles, Paperclip, Mic, ArrowUp } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ChatMessage {\n from: \"bot\" | \"me\";\n text: React.ReactNode;\n}\n\nconst DEFAULT_MESSAGES: ChatMessage[] = [\n { from: \"bot\", text: \"Hey, I'm Vector. Ask me to draft, summarize, or name anything.\" },\n { from: \"me\", text: \"Write a title for a late-night ambient mix.\" },\n { from: \"bot\", text: 'How about \"Low Tide, 3AM\"? I can give you a few more.' },\n];\n\nconst DEFAULT_SUGGESTIONS = [\"More title ideas\", \"Make it moodier\", \"Write a blurb\"];\n\nexport interface AIChatProps extends React.HTMLAttributes {\n name?: string;\n role?: string;\n placeholder?: string;\n messages?: ChatMessage[];\n suggestions?: string[];\n}\n\n/** Structured Liquidity AI chat — a rigid conversation panel with glass bubbles, animated by the kit script. */\nexport const AIChat = React.forwardRef(\n (\n {\n name = \"Vector\",\n role = \"AI assistant\",\n placeholder = \"Ask Vector anything\",\n messages = DEFAULT_MESSAGES,\n suggestions = DEFAULT_SUGGESTIONS,\n className,\n ...props\n },\n ref,\n ) => (\n \n
\n \n \n \n \n {name} \n {role} \n \n \n
\n
\n {messages.map((m, i) => (\n
\n {m.from === \"bot\" && (\n
\n \n \n )}\n
{m.text}
\n
\n ))}\n
\n
\n {suggestions.map((s, i) => (\n \n {s}\n \n ))}\n
\n
\n
\n ),\n);\nAIChat.displayName = \"AIChat\";\n",
+ "content": "import * as React from \"react\";\nimport { Sparkles, Paperclip, Mic, ArrowUp } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ChatMessage {\n from: \"bot\" | \"me\";\n text: React.ReactNode;\n}\n\nconst DEFAULT_MESSAGES: ChatMessage[] = [\n {\n from: \"bot\",\n text: \"Hey, I'm Vector. Ask me to draft, summarize, or name anything.\",\n },\n { from: \"me\", text: \"Summarize the latest transit observation.\" },\n {\n from: \"bot\",\n text: \"One stable orbit, two anomalies, and a clean signal window at 03:20 UTC.\",\n },\n];\n\nconst DEFAULT_SUGGESTIONS = [\"Show anomalies\", \"Compare orbits\", \"Draft a log\"];\n\nexport interface AIChatProps extends React.HTMLAttributes {\n name?: string;\n role?: string;\n placeholder?: string;\n messages?: ChatMessage[];\n suggestions?: string[];\n}\n\n/** Structured Liquidity AI chat — a rigid conversation panel with glass bubbles, animated by the kit script. */\nexport const AIChat = React.forwardRef(\n (\n {\n name = \"Vector\",\n role = \"AI assistant\",\n placeholder = \"Ask Vector anything\",\n messages = DEFAULT_MESSAGES,\n suggestions = DEFAULT_SUGGESTIONS,\n className,\n ...props\n },\n ref,\n ) => (\n \n
\n \n \n \n \n {name} \n {role} \n \n \n
\n
\n {messages.map((m, i) => (\n
\n {m.from === \"bot\" && (\n
\n \n \n )}\n
{m.text}
\n
\n ))}\n
\n
\n {suggestions.map((s, i) => (\n \n {s}\n \n ))}\n
\n
\n
\n ),\n);\nAIChat.displayName = \"AIChat\";\n",
"type": "registry:ui",
"target": "components/ui/ai-chat.tsx"
}
diff --git a/artifacts/structured-liquidity/public/r/media-player.json b/artifacts/structured-liquidity/public/r/media-player.json
index 1922e68..5c286ea 100644
--- a/artifacts/structured-liquidity/public/r/media-player.json
+++ b/artifacts/structured-liquidity/public/r/media-player.json
@@ -17,7 +17,7 @@
"files": [
{
"path": "registry/ui/media-player.tsx",
- "content": "import * as React from \"react\";\nimport {\n Radio,\n Shuffle,\n SkipBack,\n Pause,\n SkipForward,\n Repeat,\n Volume2,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Waveform } from \"@/components/ui/waveform\";\n\nexport interface MediaPlayerProps extends Omit<\n React.HTMLAttributes,\n \"onVolumeChange\"\n> {\n eyebrow?: string;\n title?: string;\n artist?: string;\n badge?: React.ReactNode;\n elapsed?: string;\n duration?: string;\n /** waveform bar count */\n bars?: number;\n /** leading bars read as elapsed */\n played?: number;\n /** volume fill, 0–100 */\n volume?: number;\n onVolumeChange?: (value: number) => void;\n}\n\n/** Structured Liquidity media player — a rigid \"now playing\" card over a glass waveform. */\nexport const MediaPlayer = React.forwardRef(\n (\n {\n eyebrow = \"Now playing\",\n title = \"Gracias a la Vida\",\n artist = \"Violeta Parra\",\n badge = \"Live\",\n elapsed = \"1:48\",\n duration = \"4:38\",\n bars = 56,\n played = 22,\n volume = 72,\n onVolumeChange,\n className,\n ...props\n },\n ref,\n ) => {\n const [internalVolume, setInternalVolume] = React.useState(volume);\n React.useEffect(() => setInternalVolume(volume), [volume]);\n const level = internalVolume;\n const updateVolume = (next: number) => {\n setInternalVolume(next);\n onVolumeChange?.(next);\n };\n\n return (\n \n
\n
\n \n \n
\n {eyebrow} \n {title} \n {artist} \n
\n {badge != null &&
{badge} }\n
\n
\n
\n
\n {elapsed} \n {duration} \n
\n
\n
\n
\n
\n \n \n
\n \n \n
\n \n \n
\n \n \n
\n \n \n
\n
\n
\n
\n Volume \n \n updateVolume(Number(event.target.value))}\n />\n \n
\n
\n
\n );\n },\n);\nMediaPlayer.displayName = \"MediaPlayer\";\n",
+ "content": "import * as React from \"react\";\nimport {\n Radio,\n Shuffle,\n SkipBack,\n Pause,\n SkipForward,\n Repeat,\n Volume2,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Waveform } from \"@/components/ui/waveform\";\n\nexport interface MediaPlayerProps extends Omit<\n React.HTMLAttributes,\n \"onVolumeChange\"\n> {\n eyebrow?: string;\n title?: string;\n artist?: string;\n badge?: React.ReactNode;\n elapsed?: string;\n duration?: string;\n /** waveform bar count */\n bars?: number;\n /** leading bars read as elapsed */\n played?: number;\n /** volume fill, 0–100 */\n volume?: number;\n onVolumeChange?: (value: number) => void;\n}\n\n/** Structured Liquidity media player — a rigid \"now playing\" card over a glass waveform. */\nexport const MediaPlayer = React.forwardRef(\n (\n {\n eyebrow = \"Now playing\",\n title = \"Transit Echo 04\",\n artist = \"Kepler Station Archive\",\n badge = \"Live\",\n elapsed = \"1:48\",\n duration = \"4:38\",\n bars = 56,\n played = 22,\n volume = 72,\n onVolumeChange,\n className,\n ...props\n },\n ref,\n ) => {\n const [internalVolume, setInternalVolume] = React.useState(volume);\n React.useEffect(() => setInternalVolume(volume), [volume]);\n const level = internalVolume;\n const updateVolume = (next: number) => {\n setInternalVolume(next);\n onVolumeChange?.(next);\n };\n\n return (\n \n
\n
\n \n \n
\n {eyebrow} \n {title} \n {artist} \n
\n {badge != null &&
{badge} }\n
\n
\n
\n
\n {elapsed} \n {duration} \n
\n
\n
\n
\n
\n \n \n
\n \n \n
\n \n \n
\n \n \n
\n \n \n
\n
\n
\n
\n Volume \n \n updateVolume(Number(event.target.value))}\n />\n \n
\n
\n
\n );\n },\n);\nMediaPlayer.displayName = \"MediaPlayer\";\n",
"type": "registry:ui",
"target": "components/ui/media-player.tsx"
}
diff --git a/artifacts/structured-liquidity/public/r/mobile-nav.json b/artifacts/structured-liquidity/public/r/mobile-nav.json
index b7d5c9b..ce120ec 100644
--- a/artifacts/structured-liquidity/public/r/mobile-nav.json
+++ b/artifacts/structured-liquidity/public/r/mobile-nav.json
@@ -14,7 +14,7 @@
"files": [
{
"path": "registry/ui/mobile-nav.tsx",
- "content": "import type { CSSProperties } from \"react\";\nimport * as React from \"react\";\nimport {\n Menu,\n X,\n Search,\n Compass,\n Library,\n Radio,\n Heart,\n House,\n User,\n Disc3,\n Music,\n BarChart3,\n Sparkles,\n ChevronLeft,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction PhoneScreen() {\n return (\n \n );\n}\n\n/** Mobile navigation — a phone frame with a top drawer and one floating liquid-glass tab bar. */\nexport const MobileTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [menuOpen, setMenuOpen] = React.useState(false);\n const [active, setActive] = React.useState(0);\n const select = (index: number) => {\n setActive(index);\n setMenuOpen(false);\n };\n\n return (\n \n
\n
\n setMenuOpen((open) => !open)}\n >\n \n \n \n \n \n universe\n \n \n \n \n
\n
\n
\n select(0)}\n >\n \n Discover\n \n select(1)}\n >\n \n Library\n \n select(2)}\n >\n \n Radio\n \n select(3)}\n >\n \n Saved\n \n
\n
\n
\n
\n \n select(0)}\n >\n \n Home \n \n select(1)}\n >\n \n Search \n \n select(2)}\n >\n \n Browse \n \n select(3)}\n >\n \n Saved \n \n select(4)}\n >\n \n You \n \n \n
\n
\n );\n});\nMobileTabBar.displayName = \"MobileTabBar\";\n\n/** Mobile navigation — a floating glass tab bar with a sliding active marker. */\nexport const GlassTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n \n
\n
\n
\n \n setActive(0)}\n >\n \n Playback \n \n setActive(1)}\n >\n \n Music \n \n setActive(2)}\n >\n \n Billboard \n \n setActive(3)}\n >\n \n Lyriq \n \n \n
\n
\n );\n});\nGlassTabBar.displayName = \"GlassTabBar\";\n\n/** Mobile navigation — a segmented sub-nav header with a sliding marker (wired by the kit script). */\nexport const SegmentedNav = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n \n
\n
\n \n \n \n Library \n \n \n \n
\n
\n \n setActive(0)}\n >\n Playlists\n \n setActive(1)}\n >\n Songs\n \n
\n
\n
\n
\n );\n});\nSegmentedNav.displayName = \"SegmentedNav\";\n",
+ "content": "import type { CSSProperties } from \"react\";\nimport * as React from \"react\";\nimport {\n Menu,\n X,\n Search,\n Compass,\n Library,\n Radio,\n Heart,\n House,\n User,\n Disc3,\n Music,\n BarChart3,\n Sparkles,\n ChevronLeft,\n} from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nfunction PhoneScreen() {\n return (\n \n );\n}\n\n/** Mobile navigation — a phone frame with a top drawer and one floating liquid-glass tab bar. */\nexport const MobileTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [menuOpen, setMenuOpen] = React.useState(false);\n const [active, setActive] = React.useState(0);\n const select = (index: number) => {\n setActive(index);\n setMenuOpen(false);\n };\n\n return (\n \n
\n
\n setMenuOpen((open) => !open)}\n >\n \n \n \n \n \n Kepler\n \n \n \n \n
\n
\n
\n select(0)}\n >\n \n Mission\n \n select(1)}\n >\n \n Signals\n \n select(2)}\n >\n \n Atlas\n \n select(3)}\n >\n \n Archive\n \n
\n
\n
\n
\n \n select(0)}\n >\n \n Mission \n \n select(1)}\n >\n \n Signals \n \n select(2)}\n >\n \n Atlas \n \n select(3)}\n >\n \n Archive \n \n select(4)}\n >\n \n Crew \n \n \n
\n
\n );\n});\nMobileTabBar.displayName = \"MobileTabBar\";\n\n/** Mobile navigation — a floating glass tab bar with a sliding active marker. */\nexport const GlassTabBar = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n \n
\n
\n
\n \n setActive(0)}\n >\n \n Mission \n \n setActive(1)}\n >\n \n Signals \n \n setActive(2)}\n >\n \n Atlas \n \n setActive(3)}\n >\n \n Archive \n \n \n
\n
\n );\n});\nGlassTabBar.displayName = \"GlassTabBar\";\n\n/** Mobile navigation — a segmented sub-nav header with a sliding marker (wired by the kit script). */\nexport const SegmentedNav = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => {\n const [active, setActive] = React.useState(0);\n return (\n \n
\n
\n \n \n \n Signal archive \n \n \n \n
\n
\n \n setActive(0)}\n >\n Observations\n \n setActive(1)}\n >\n Transits\n \n
\n
\n
\n
\n );\n});\nSegmentedNav.displayName = \"SegmentedNav\";\n",
"type": "registry:ui",
"target": "components/ui/mobile-nav.tsx"
}
diff --git a/artifacts/structured-liquidity/public/r/site-templates.json b/artifacts/structured-liquidity/public/r/site-templates.json
index 3a16c5e..70afd09 100644
--- a/artifacts/structured-liquidity/public/r/site-templates.json
+++ b/artifacts/structured-liquidity/public/r/site-templates.json
@@ -12,7 +12,7 @@
"files": [
{
"path": "registry/templates/site-templates.tsx",
- "content": "import * as React from \"react\";\nimport { FrameworkAttribution } from \"@/components/ui/attribution\";\nimport { SiteContent, SiteShell } from \"@/components/ui/site-shell\";\n\ninterface TemplateProps {\n navigation?: React.ReactNode;\n footer?: React.ReactNode;\n children: React.ReactNode;\n}\n\ninterface MarketingSiteTemplateProps extends TemplateProps {\n hero: React.ReactNode;\n proof?: React.ReactNode;\n}\n\n/** Landing template distilled from Interspace, 2 Days Early, and Bumble Bee. */\nexport function MarketingSiteTemplate({\n navigation,\n hero,\n proof,\n children,\n footer,\n}: MarketingSiteTemplateProps) {\n return (\n \n {navigation} \n \n {hero}\n {proof}\n {children}\n \n \n {footer ?? }\n \n
\n );\n}\n\ninterface DashboardTemplateProps extends TemplateProps {\n metrics?: React.ReactNode;\n filters?: React.ReactNode;\n}\n\n/** Data-heavy template distilled from Interspace portfolio and index views. */\nexport function DashboardTemplate({\n navigation,\n metrics,\n filters,\n children,\n footer,\n}: DashboardTemplateProps) {\n return (\n \n {navigation} \n \n {metrics}\n {filters}\n {children} \n \n \n {footer ?? }\n \n
\n );\n}\n\ninterface GuidedFlowTemplateProps extends TemplateProps {\n steps: React.ReactNode;\n}\n\n/** Multi-step conversion template distilled from Bumble Bee enrollment. */\nexport function GuidedFlowTemplate({\n navigation,\n steps,\n children,\n footer,\n}: GuidedFlowTemplateProps) {\n return (\n \n
{navigation} \n
\n \n {children} \n \n
\n {footer ?? }\n \n
\n );\n}\n\ninterface ImmersiveAppTemplateProps extends TemplateProps {\n hud?: React.ReactNode;\n controls?: React.ReactNode;\n}\n\n/** Full-viewport app template distilled from Cosmograph and Universe. */\nexport function ImmersiveAppTemplate({\n navigation,\n hud,\n controls,\n children,\n footer,\n}: ImmersiveAppTemplateProps) {\n return (\n \n {navigation} \n \n {children}\n {hud}\n {controls}\n \n \n {footer ?? }\n \n
\n );\n}\n\n",
+ "content": "import * as React from \"react\";\nimport { FrameworkAttribution } from \"@/components/ui/attribution\";\nimport { SiteContent, SiteShell } from \"@/components/ui/site-shell\";\n\ninterface TemplateProps {\n navigation?: React.ReactNode;\n footer?: React.ReactNode;\n children: React.ReactNode;\n}\n\ninterface MarketingSiteTemplateProps extends TemplateProps {\n hero: React.ReactNode;\n proof?: React.ReactNode;\n}\n\n/** Landing template for a concise public narrative, proof, and conversion path. */\nexport function MarketingSiteTemplate({\n navigation,\n hero,\n proof,\n children,\n footer,\n}: MarketingSiteTemplateProps) {\n return (\n \n {navigation} \n \n {hero}\n {proof}\n {children}\n \n \n {footer ?? }\n \n
\n );\n}\n\ninterface DashboardTemplateProps extends TemplateProps {\n metrics?: React.ReactNode;\n filters?: React.ReactNode;\n}\n\n/** Data-heavy template for metrics, filters, and responsive evidence views. */\nexport function DashboardTemplate({\n navigation,\n metrics,\n filters,\n children,\n footer,\n}: DashboardTemplateProps) {\n return (\n \n {navigation} \n \n {metrics}\n {filters}\n {children} \n \n \n {footer ?? }\n \n
\n );\n}\n\ninterface GuidedFlowTemplateProps extends TemplateProps {\n steps: React.ReactNode;\n}\n\n/** Multi-step conversion template with persistent progress and recovery states. */\nexport function GuidedFlowTemplate({\n navigation,\n steps,\n children,\n footer,\n}: GuidedFlowTemplateProps) {\n return (\n \n
{navigation} \n
\n \n {children} \n \n
\n {footer ?? }\n \n
\n );\n}\n\ninterface ImmersiveAppTemplateProps extends TemplateProps {\n hud?: React.ReactNode;\n controls?: React.ReactNode;\n}\n\n/** Full-viewport app template for a canvas, HUD overlays, and command controls. */\nexport function ImmersiveAppTemplate({\n navigation,\n hud,\n controls,\n children,\n footer,\n}: ImmersiveAppTemplateProps) {\n return (\n \n {navigation} \n \n {children}\n {hud}\n {controls}\n \n \n {footer ?? }\n \n
\n );\n}\n",
"type": "registry:block",
"target": "components/templates/site-templates.tsx"
}
diff --git a/artifacts/structured-liquidity/src/components/site/Components.tsx b/artifacts/structured-liquidity/src/components/site/Components.tsx
index 988be4b..ded789c 100644
--- a/artifacts/structured-liquidity/src/components/site/Components.tsx
+++ b/artifacts/structured-liquidity/src/components/site/Components.tsx
@@ -25,7 +25,9 @@ export function Components() {
A curated view of the patterns that carry a distinct job in a real
product. The full installable registry remains available; this gallery
favors useful composition, working state, and motion you can test by
- clicking, typing, toggling, and opening.
+ clicking, typing, toggling, and opening. Demo content belongs to the
+ fictional Kepler Station survey, keeping product examples coherent
+ without borrowing private language from live applications.
diff --git a/artifacts/structured-liquidity/src/components/site/Nav.tsx b/artifacts/structured-liquidity/src/components/site/Nav.tsx
index 179766a..e73b45b 100644
--- a/artifacts/structured-liquidity/src/components/site/Nav.tsx
+++ b/artifacts/structured-liquidity/src/components/site/Nav.tsx
@@ -1,5 +1,13 @@
-import { useCallback, useEffect, useRef } from "react";
-import { Bot, Blocks, Github, Sparkles, Waypoints } from "lucide-react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import {
+ Bot,
+ Blocks,
+ Github,
+ Menu,
+ Sparkles,
+ Waypoints,
+ X,
+} from "lucide-react";
import { Hypercube } from "./liquid";
import { InstallButton } from "./InstallButton";
@@ -14,6 +22,7 @@ const NAV_ITEMS = [
export function Nav() {
const navRef = useRef(null);
const linksRef = useRef(null);
+ const [mobileOpen, setMobileOpen] = useState(false);
const positionMarker = useCallback((index: number) => {
const links = linksRef.current;
@@ -33,7 +42,10 @@ export function Nav() {
const page = document.documentElement;
const distance = Math.max(1, page.scrollHeight - window.innerHeight);
const progress = Math.min(1, Math.max(0, window.scrollY / distance));
- navRef.current?.style.setProperty("--page-progress", `${progress * 100}%`);
+ navRef.current?.style.setProperty(
+ "--page-progress",
+ `${progress * 100}%`,
+ );
};
const requestUpdate = () => {
if (!frame) frame = window.requestAnimationFrame(update);
@@ -48,19 +60,39 @@ export function Nav() {
};
}, [positionMarker]);
+ useEffect(() => {
+ if (!mobileOpen) return;
+ const closeOnEscape = (event: KeyboardEvent) => {
+ if (event.key === "Escape") setMobileOpen(false);
+ };
+ window.addEventListener("keydown", closeOnEscape);
+ return () => window.removeEventListener("keydown", closeOnEscape);
+ }, [mobileOpen]);
+
return (
Structured Liquidity
+ setMobileOpen((open) => !open)}
+ >
+ {mobileOpen ? : }
+ {mobileOpen ? "Close" : "Menu"}
+
positionMarker(0)}
onBlur={(event) => {
- if (!event.currentTarget.contains(event.relatedTarget)) positionMarker(0);
+ if (!event.currentTarget.contains(event.relatedTarget))
+ positionMarker(0);
}}
>
@@ -80,7 +112,38 @@ export function Nav() {
+
+
@@ -79,21 +97,35 @@ export function KitActions() {
Toolbar
-
-
-
+
+
+
+
+
+
+
+
+
- Link
+
+
+ Insert link
+
-
);
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx
index cd9eb22..9ca1f98 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitContentData.tsx
@@ -61,12 +61,12 @@ export function KitContentData() {
Empty
- ♪
- No tracks yet
+ ✦
+ No signals yet
- Import audio to start a library.
+ Connect an instrument to begin the survey.
- Import
+ Connect
@@ -76,8 +76,8 @@ export function KitContentData() {
-
U
- universe.audio
- Audio platform · Live
+ Kepler Station
+ Orbital survey · Live
Open
@@ -94,8 +94,8 @@ export function KitContentData() {
SL
- Structured Liquidity
- Design language
+ Asteria Atlas
+ Survey catalogue
Open
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitDataDisplay.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitDataDisplay.tsx
index cb818e6..f95af9b 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitDataDisplay.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitDataDisplay.tsx
@@ -50,25 +50,25 @@ export function KitDataDisplay() {
100+
- Languages
+ Survey sectors
1M+
- Songs
+ Signals indexed
2.7K
- Words translated
+ Spectra classified
200+
- Operators in syndicate
+ Crew online
60%
- Scaled 0–100 FTEs
+ Survey complete
@@ -87,7 +87,9 @@ export function KitDataDisplay() {
Build failed
-
Border weight must be ≥ 1px.
+
+ Border weight must be ≥ 1px.
+
@@ -97,14 +99,25 @@ export function KitDataDisplay() {
Avatar
SL
-
+
JD
A
- B
- C
- +5
+
+ B
+
+
+ C
+
+
+ +5
+
@@ -119,23 +132,23 @@ export function KitDataDisplay() {
- U
+ KS
- universe.audio
+ Kepler Station
- Built on SL
+ Survey nominal
}
>
- @universe
+ Station profile
@@ -167,7 +180,11 @@ export function KitDataDisplay() {
Waveform
-
+
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx
index 69046d3..d07fc72 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitFieldsControls.tsx
@@ -1,18 +1,29 @@
+import { ButtonGroup, ButtonGroupItem } from "@/components/ui/button-group";
import {
- ButtonGroup,
- ButtonGroupItem,
-} from "@/components/ui/button-group";
-import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field";
-import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
+ Field,
+ FieldLabel,
+ FieldDescription,
+ FieldError,
+} from "@/components/ui/field";
+import {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupInput,
+} from "@/components/ui/input-group";
import { Kbd } from "@/components/ui/kbd";
import { Spinner } from "@/components/ui/spinner";
export function KitFieldsControls() {
return (
-
Fields & controls Button group · Field · Input group · Kbd · Spinner
+
+ Fields & controls
+
+
+ Button group · Field · Input group · Kbd · Spinner
+
+
@@ -51,24 +62,47 @@ export function KitFieldsControls() {
Kbd
- Command
- ⌘ K
+
+ Command
+
+
+ ⌘ K
+
- Save
- ⌘ S
+
+ Save
+
+
+ ⌘ S
+
);
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx
index 184282e..d6b946c 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitFormsInputs.tsx
@@ -337,14 +337,14 @@ export function KitFormsInputs() {
Fieldset
- Billing address
+ Station coordinates
- City
-
+ Sector
+
- Postal code
-
+ Orbit code
+
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx
index 5ebedff..5e8569a 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitLayoutStructure.tsx
@@ -45,7 +45,7 @@ export function KitLayoutStructure() {
Purpose
- By operators. For operators.
+ Signals, ordered for the mission.
@@ -55,12 +55,12 @@ export function KitLayoutStructure() {
@@ -93,13 +93,13 @@ export function KitLayoutStructure() {
Scroll area
- Track 01 · Aurora
- Track 02 · Meniscus
- Track 03 · Offset
- Track 04 · Specular
- Track 05 · Viscosity
- Track 06 · Containment
- Track 07 · Refraction
+ Orbit 01 · Periapsis
+ Orbit 02 · Transit
+ Orbit 03 · Eclipse
+ Orbit 04 · Relay
+ Orbit 05 · Survey
+ Orbit 06 · Return
+ Orbit 07 · Archive
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx
index 35a55f8..51a3819 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitMenusOverlays.tsx
@@ -32,18 +32,37 @@ export function KitMenusOverlays() {
Menus & overlays
- Alert dialog · Context menu · Drawer · Navigation menu
+
+ Alert dialog · Context menu · Drawer · Navigation menu
+
-
Alert dialog
-
A blocking confirm for destructive actions.
+
+ A blocking confirm for destructive actions.
+
- Delete project
+
+ Delete project
+
- Delete project?
- This permanently removes the project and all of its data. This action cannot be undone.
+
+ Delete project?
+
+
+ This permanently removes the project and all of its data. This
+ action cannot be undone.
+
Cancel
Delete project
@@ -54,7 +73,10 @@ export function KitMenusOverlays() {
Context menu
-
+
Right-click surface
@@ -70,12 +92,30 @@ export function KitMenusOverlays() {
Drawer
-
A panel that rises from the bottom edge.
+
+ A panel that rises from the bottom edge.
+
- Open drawer
+
+ Open drawer
+
- Quick settings
- A panel that rises from the bottom edge, settling into its flat shadow.
+
+ Quick settings
+
+
+ A panel that rises from the bottom edge, settling into its flat
+ shadow.
+
Close
Save
@@ -90,9 +130,11 @@ export function KitMenusOverlays() {
Products
- universe.audio
- Structured Liquidity
- Interspace
+
+ Mission control
+
+ Orbital atlas
+ Signal archive
@@ -104,12 +146,18 @@ export function KitMenusOverlays() {
- Pricing
+
+ Pricing
+
- Hover an item for its flyout.
+
+ Hover an item for its flyout.
+
-
);
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx
index 63d5e0b..64cb750 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitNavDisclosure.tsx
@@ -38,8 +38,8 @@ export function KitNavDisclosure() {
Is it tied to one product?
- No. universe.audio is only one example; the language is
- product-agnostic and travels from product to product by its
+ No. Kepler Station is a fictional specimen world; the language
+ is product-agnostic and travels between products through its
tokens.
diff --git a/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx b/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx
index b876bed..7d06228 100644
--- a/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx
+++ b/artifacts/structured-liquidity/src/components/site/kit/KitPatterns.tsx
@@ -78,7 +78,9 @@ export function KitPatterns() {
Site shell
- Navigation and footer use the shared site frame
+
+ Navigation and footer use the shared site frame
+
Body surface aligns to the same frame
@@ -124,10 +126,10 @@ export function KitPatterns() {
- Fintech
+ Spectral
- New York
+ Kepler-186
26 results
@@ -173,11 +175,11 @@ export function KitPatterns() {
Verified family review
- “The same familiar faces, thoughtful updates, and a place our
- child is excited to return to.”
+ “The transit log made a complex orbital shift immediately clear to
+ every crew on the station.”
- Brooklyn parent Daycare portal
+ Mission review Flight archive
@@ -186,10 +188,10 @@ export function KitPatterns() {
Logo grid
{[
- ["IV", "Advisory"],
- ["2D", "Syndicate"],
- ["BB", "Daycare"],
- ["CG", "Research"],
+ ["KS", "Station"],
+ ["AT", "Atlas"],
+ ["SR", "Survey"],
+ ["RL", "Relay"],
].map(([mark, label]) => (
{mark}
@@ -206,11 +208,11 @@ export function KitPatterns() {
01
- Investor update published
+ Transit report published
Today · 09:42
- Operating notes and portfolio metrics are ready for review.
+ Orbital notes and instrument readings are ready for review.
@@ -248,7 +250,7 @@ export function KitPatterns() {
Pricing card
- Operator
+ Expedition
Recommended
@@ -273,7 +275,7 @@ export function KitPatterns() {
- Choose Operator
+ Choose Expedition
@@ -281,32 +283,32 @@ export function KitPatterns() {
Unit economics chart
);
}
-
diff --git a/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx b/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx
index 356b87a..7187208 100644
--- a/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx
+++ b/artifacts/structured-liquidity/src/components/ui/ai-chat.tsx
@@ -8,12 +8,18 @@ interface ChatMessage {
}
const DEFAULT_MESSAGES: ChatMessage[] = [
- { from: "bot", text: "Hey, I'm Vector. Ask me to draft, summarize, or name anything." },
- { from: "me", text: "Write a title for a late-night ambient mix." },
- { from: "bot", text: 'How about "Low Tide, 3AM"? I can give you a few more.' },
+ {
+ from: "bot",
+ text: "Hey, I'm Vector. Ask me to draft, summarize, or name anything.",
+ },
+ { from: "me", text: "Summarize the latest transit observation." },
+ {
+ from: "bot",
+ text: "One stable orbit, two anomalies, and a clean signal window at 03:20 UTC.",
+ },
];
-const DEFAULT_SUGGESTIONS = ["More title ideas", "Make it moodier", "Write a blurb"];
+const DEFAULT_SUGGESTIONS = ["Show anomalies", "Compare orbits", "Draft a log"];
export interface AIChatProps extends React.HTMLAttributes {
name?: string;
diff --git a/artifacts/structured-liquidity/src/components/ui/media-player.tsx b/artifacts/structured-liquidity/src/components/ui/media-player.tsx
index 3fcc1f7..1a96baa 100644
--- a/artifacts/structured-liquidity/src/components/ui/media-player.tsx
+++ b/artifacts/structured-liquidity/src/components/ui/media-player.tsx
@@ -37,8 +37,8 @@ export const MediaPlayer = React.forwardRef(
(
{
eyebrow = "Now playing",
- title = "Gracias a la Vida",
- artist = "Violeta Parra",
+ title = "Transit Echo 04",
+ artist = "Kepler Station Archive",
badge = "Live",
elapsed = "1:48",
duration = "4:38",
diff --git a/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx b/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx
index af4bc95..ae029a8 100644
--- a/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx
+++ b/artifacts/structured-liquidity/src/components/ui/mobile-nav.tsx
@@ -60,7 +60,7 @@ export const MobileTabBar = React.forwardRef<
style={{ "--s": "18px" } as CSSProperties}
aria-hidden="true"
/>
- universe
+ Kepler
@@ -74,7 +74,7 @@ export const MobileTabBar = React.forwardRef<
onClick={() => select(0)}
>
- Discover
+ Mission
select(1)}
>
- Library
+ Signals
select(2)}
>
- Radio
+ Atlas
select(3)}
>
- Saved
+ Archive
@@ -115,7 +115,7 @@ export const MobileTabBar = React.forwardRef<
onClick={() => select(0)}
>
- Home
+ Mission
select(1)}
>
- Search
+ Signals
select(2)}
>
- Browse
+ Atlas
select(3)}
>
- Saved
+ Archive
select(4)}
>
- You
+ Crew
@@ -178,7 +178,7 @@ export const GlassTabBar = React.forwardRef<
onClick={() => setActive(0)}
>
-
Playback
+
Mission
setActive(1)}
>
- Music
+ Signals
setActive(2)}
>
- Billboard
+ Atlas
setActive(3)}
>
- Lyriq
+ Archive
@@ -224,7 +224,7 @@ export const SegmentedNav = React.forwardRef<
- Library
+ Signal archive
@@ -243,7 +243,7 @@ export const SegmentedNav = React.forwardRef<
aria-selected={active === 0}
onClick={() => setActive(0)}
>
- Playlists
+ Observations
setActive(1)}
>
- Songs
+ Transits
diff --git a/artifacts/structured-liquidity/src/styles/40-inline.css b/artifacts/structured-liquidity/src/styles/40-inline.css
index b99992f..8adf3e4 100644
--- a/artifacts/structured-liquidity/src/styles/40-inline.css
+++ b/artifacts/structured-liquidity/src/styles/40-inline.css
@@ -1567,6 +1567,10 @@ body {
.nav .nav-utility { padding-inline: 0.58rem; }
}
@media (max-width: 600px) {
+ html,
+ body {
+ overflow-x: clip;
+ }
#components .kit-grid.gallery { column-count: 1; }
.nav .nav-utility { font-size: 0; gap: 0; }
.tenet {
@@ -1637,3 +1641,337 @@ body {
text-align: left;
}
}
+/* ============================================================
+ MOBILE AUDIT — labeled navigation, left-anchored hierarchy,
+ and specimens that stay inside the scientific grid.
+ ============================================================ */
+.nav-mobile-trigger,
+.nav-mobile-menu {
+ display: none;
+}
+
+@media (max-width: 780px) {
+ .nav {
+ padding: 0.42rem 0.48rem 0.42rem 0.7rem;
+ overflow: visible;
+ }
+ .nav .brand .name {
+ display: inline;
+ font-size: 0.88rem;
+ }
+ .nav-cluster {
+ display: none;
+ }
+ .nav-mobile-trigger {
+ display: inline-flex;
+ min-height: 44px;
+ align-items: center;
+ justify-content: center;
+ gap: 0.45rem;
+ padding: 0.58rem 0.72rem;
+ border: 1px solid rgb(var(--edge));
+ background: color-mix(in srgb, var(--ink) 9%, var(--bg-2));
+ color: var(--ink);
+ font: 700 0.74rem/1 var(--body);
+ cursor: pointer;
+ }
+ .nav-mobile-trigger svg {
+ width: 1rem;
+ height: 1rem;
+ }
+ .nav-mobile-trigger[aria-expanded="true"] {
+ background: var(--accent);
+ color: var(--accent-ink);
+ }
+ .nav-mobile-menu {
+ position: absolute;
+ top: calc(100% + 0.45rem);
+ left: 0;
+ right: 0;
+ display: block;
+ max-height: 0;
+ overflow: hidden;
+ visibility: hidden;
+ opacity: 0;
+ transform: translateY(-0.5rem);
+ border: 0 solid rgb(var(--edge));
+ background: color-mix(in srgb, var(--bg-2) 96%, transparent);
+ backdrop-filter: blur(var(--glass-blur)) saturate(150%);
+ -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%);
+ transition:
+ opacity var(--motion-fast) var(--ease-liquid),
+ transform var(--motion) var(--ease-liquid-over),
+ visibility 0s linear var(--motion);
+ }
+ .nav-mobile-menu[data-open="true"] {
+ max-height: 28rem;
+ visibility: visible;
+ opacity: 1;
+ transform: none;
+ border-width: 1px;
+ box-shadow: 5px 5px 0 var(--hard-shadow);
+ transition-delay: 0s;
+ }
+ .nav-mobile-links {
+ min-height: 0;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ overflow: hidden;
+ }
+ .nav-mobile-links a {
+ display: flex;
+ min-height: 50px;
+ align-items: center;
+ gap: 0.65rem;
+ padding: 0.75rem;
+ border-right: 1px solid color-mix(in srgb, var(--ink) 24%, transparent);
+ border-bottom: 1px solid color-mix(in srgb, var(--ink) 24%, transparent);
+ color: var(--ink);
+ font: 700 0.82rem/1.2 var(--body);
+ text-decoration: none;
+ }
+ .nav-mobile-links a:nth-child(2n) {
+ border-right: 0;
+ }
+ .nav-mobile-links svg {
+ width: 1rem;
+ height: 1rem;
+ color: var(--accent);
+ }
+ .nav-mobile-utilities {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.5rem;
+ padding: 0.65rem;
+ }
+ .nav-mobile-utilities .nav-utility {
+ min-height: 44px;
+ justify-content: center;
+ padding: 0.65rem;
+ font-size: 0.78rem;
+ }
+ .section-head,
+ .section-head .eyebrow,
+ .section-head .section-title,
+ .section-head .lead,
+ .tenet,
+ .tenet h3,
+ .tenet > p,
+ .tenet li,
+ .kit-group-head,
+ .kit-cell,
+ .foot {
+ text-align: left;
+ }
+ .kit-group > .kit-group-head {
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 0.4rem;
+ }
+ .kit-group-head .kg-rule {
+ width: 100%;
+ flex: none;
+ }
+ .kit-group-head .kg-count {
+ line-height: 1.5;
+ }
+}
+
+@media (max-width: 600px) {
+ section {
+ padding-block: clamp(2.8rem, 12vw, 4rem);
+ }
+ .wrap {
+ width: calc(100vw - 1.5rem);
+ }
+ .hero {
+ padding-top: 6.4rem;
+ }
+ .hero-cube-wrap {
+ right: -7.5rem;
+ }
+ #components .kit-grid.gallery,
+ .kit-grid {
+ width: 100%;
+ min-width: 0;
+ }
+ #components .kit-grid.gallery .kit-cell,
+ .kit-cell {
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
+ padding: 1rem;
+ overflow: hidden;
+ }
+ .kit-cell > *,
+ .kit-row,
+ .kit-col {
+ min-width: 0;
+ max-width: 100%;
+ }
+ .browser-bar {
+ min-width: 0;
+ }
+ .browser-bar .url {
+ min-width: 0;
+ margin-left: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ #adopt .kit-cell p,
+ #adopt .kit-cell code {
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ }
+ .sl-button-specimen .sl-btn {
+ width: 100%;
+ justify-content: flex-start;
+ }
+ .sl-navbar {
+ align-items: flex-start;
+ }
+ .sl-navbar .nb-brand {
+ width: 100%;
+ }
+ .sl-navbar .nb-links {
+ width: 100%;
+ margin-left: 0;
+ }
+ .sl-navbar .nb-link {
+ min-width: 0;
+ padding-inline: 0.35rem;
+ font-size: 0.68rem;
+ }
+ .sl-navbar .nb-link svg {
+ display: none;
+ }
+ .sl-navmenu {
+ display: grid;
+ grid-template-columns: 1fr;
+ width: 100%;
+ }
+ .sl-navmenu .nm-item {
+ min-width: 0;
+ }
+ .sl-navmenu .nm-item > button {
+ width: 100%;
+ min-height: 44px;
+ justify-content: space-between;
+ border-left: 0;
+ border-top: 1px solid rgb(var(--edge) / 0.25);
+ }
+ .sl-navmenu .nm-item:first-child > button {
+ border-top: 0;
+ }
+ .sl-navmenu .nm-panel {
+ left: 0;
+ right: 0;
+ width: 100%;
+ min-width: 0;
+ }
+ .sl-profile-card-facts > div {
+ grid-template-columns: 1fr;
+ gap: 0.2rem;
+ }
+ .sl-profile-card-facts dd {
+ text-align: left;
+ }
+ .sl-dialog .actions {
+ align-items: stretch;
+ flex-direction: column;
+ justify-content: flex-start;
+ }
+ .sl-dialog .actions > * {
+ width: 100%;
+ }
+ .sl-toast-stack {
+ left: 0.75rem;
+ right: 0.75rem;
+ width: auto;
+ }
+ .sl-otp {
+ width: 100%;
+ gap: 0.32rem;
+ }
+ .sl-otp input {
+ width: calc((100% - 1.6rem) / 6);
+ min-width: 0;
+ }
+ .sl-table-wrap,
+ .sl-table-container {
+ max-width: 100%;
+ overflow-x: auto;
+ }
+ .sl-player,
+ .sl-player .pl-top,
+ .sl-player .pl-meta,
+ .sl-player .pl-progress,
+ .sl-player .pl-controls {
+ min-width: 0;
+ max-width: 100%;
+ }
+ .sl-player {
+ overflow: hidden;
+ }
+ .sl-mnav,
+ .sl-gnav,
+ .sl-snav,
+ .mn-phone {
+ max-width: 100%;
+ }
+ .gnav-tab {
+ min-width: 0;
+ font-size: 0.5rem;
+ letter-spacing: 0;
+ }
+ .gnav-tab svg {
+ width: 16px;
+ height: 16px;
+ }
+ .sl-fab-dock {
+ bottom: max(0.65rem, env(safe-area-inset-bottom));
+ gap: 0.45rem;
+ }
+ .sl-tweaks-fab {
+ min-height: 44px;
+ padding: 0.55rem 0.68rem;
+ }
+ .twk-panel {
+ right: 0.75rem;
+ bottom: calc(4.4rem + env(safe-area-inset-bottom));
+ width: calc(100vw - 1.5rem);
+ }
+ .cta-panel h2 {
+ max-width: 100%;
+ font-size: clamp(2rem, 11.5vw, 3.1rem);
+ overflow-wrap: anywhere;
+ }
+ .sl-overlay {
+ overflow: hidden;
+ }
+}
+
+@media (max-width: 360px) {
+ .nav .brand .name {
+ display: none;
+ }
+ .nav-mobile-links {
+ grid-template-columns: 1fr;
+ }
+ .nav-mobile-links a {
+ border-right: 0;
+ }
+ .nav-mobile-utilities {
+ grid-template-columns: 1fr;
+ }
+ .gnav-tab span {
+ display: none;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .nav-mobile-menu {
+ transition: none;
+ }
+}
From d76e0162412e6403dcfc78eccb83936b94ac0c23 Mon Sep 17 00:00:00 2001
From: Samir
Date: Sun, 23 Aug 2026 22:43:19 -0400
Subject: [PATCH 4/6] feat(auth): embed staging sign-in
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Prompt: why do the clerk sign ins get redirected to clerk auth urls though? I want them to be embedded on our site with our domains etc. this should be true for every Exobase app. Shouldn’t be a random url
---
.../src/middleware/staging-access.ts | 12 +-
artifacts/structured-liquidity/package.json | 3 +
artifacts/structured-liquidity/src/App.tsx | 8 +
.../src/components/site/AuthPage.tsx | 81 ++++++++
artifacts/structured-liquidity/src/index.css | 188 ++++++++++++++++++
artifacts/structured-liquidity/src/main.tsx | 19 +-
pnpm-lock.yaml | 27 ++-
7 files changed, 331 insertions(+), 7 deletions(-)
create mode 100644 artifacts/structured-liquidity/src/components/site/AuthPage.tsx
diff --git a/artifacts/api-server/src/middleware/staging-access.ts b/artifacts/api-server/src/middleware/staging-access.ts
index f43dc62..965c989 100644
--- a/artifacts/api-server/src/middleware/staging-access.ts
+++ b/artifacts/api-server/src/middleware/staging-access.ts
@@ -106,6 +106,16 @@ function isApiRequest(req: Request): boolean {
return req.path === "/api" || req.path.startsWith("/api/");
}
+function isPublicAuthSurfaceRequest(req: Request): boolean {
+ return (
+ req.path === "/sign-in" ||
+ req.path.startsWith("/sign-in/") ||
+ req.path.startsWith("/assets/") ||
+ req.path === "/favicon.svg" ||
+ req.path === "/favicon.ico"
+ );
+}
+
function signInRedirect(config: StagingAccessConfig, req: Request): string {
const signInUrl = new URL(config.signInUrl);
const returnUrl = new URL(req.originalUrl || "/", config.canonicalOrigin);
@@ -185,7 +195,7 @@ export function installStagingAccess(app: Express): void {
);
app.use(async (req: Request, res: Response, next: NextFunction) => {
- if (isHealthRequest(req)) {
+ if (isHealthRequest(req) || isPublicAuthSurfaceRequest(req)) {
next();
return;
}
diff --git a/artifacts/structured-liquidity/package.json b/artifacts/structured-liquidity/package.json
index 7b914e1..64d3a2e 100644
--- a/artifacts/structured-liquidity/package.json
+++ b/artifacts/structured-liquidity/package.json
@@ -74,5 +74,8 @@
"vite": "catalog:",
"wouter": "^3.3.5",
"zod": "catalog:"
+ },
+ "dependencies": {
+ "@clerk/react": "^6.14.5"
}
}
diff --git a/artifacts/structured-liquidity/src/App.tsx b/artifacts/structured-liquidity/src/App.tsx
index 2a283d7..32ba3bf 100644
--- a/artifacts/structured-liquidity/src/App.tsx
+++ b/artifacts/structured-liquidity/src/App.tsx
@@ -11,8 +11,16 @@ import { Adopt } from "@/components/site/Adopt";
import { FooterCta } from "@/components/site/FooterCta";
import { Footer } from "@/components/site/Footer";
import { Tweaker } from "@/components/site/Tweaker";
+import { AuthPage } from "@/components/site/AuthPage";
export function App() {
+ if (
+ window.location.pathname === "/sign-in" ||
+ window.location.pathname.startsWith("/sign-in/")
+ ) {
+ return ;
+ }
+
const isStaging =
window.location.hostname === "staging.structured.glass" ||
import.meta.env.VITE_DEPLOYMENT_ENVIRONMENT === "staging";
diff --git a/artifacts/structured-liquidity/src/components/site/AuthPage.tsx b/artifacts/structured-liquidity/src/components/site/AuthPage.tsx
new file mode 100644
index 0000000..8f2e132
--- /dev/null
+++ b/artifacts/structured-liquidity/src/components/site/AuthPage.tsx
@@ -0,0 +1,81 @@
+import { SignIn } from "@clerk/react";
+
+const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY?.trim();
+
+export function AuthPage() {
+ if (!clerkPublishableKey) {
+ return (
+
+
+
+
Structured · Staging
+
Sign-in is not configured.
+
The embedded staging access surface is unavailable.
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
Structured · Staging
+
Enter the system.
+
+ Private review access for the Structured Liquidity design system.
+
+
+
+
Surface
+ staging.structured.glass
+
+
+
Access
+ Invited accounts only
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/artifacts/structured-liquidity/src/index.css b/artifacts/structured-liquidity/src/index.css
index 407c8ac..6f37697 100644
--- a/artifacts/structured-liquidity/src/index.css
+++ b/artifacts/structured-liquidity/src/index.css
@@ -48,3 +48,191 @@
line-height: 1;
text-transform: uppercase;
}
+
+.auth-page {
+ min-height: 100svh;
+ display: grid;
+ place-items: center;
+ padding: clamp(1rem, 4vw, 4rem);
+ background-color: var(--bg);
+ background-image:
+ linear-gradient(rgba(var(--edge), 0.055) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(var(--edge), 0.055) 1px, transparent 1px);
+ background-size: 64px 64px;
+}
+
+.auth-frame {
+ width: min(100%, 1120px);
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(360px, 0.82fr);
+ border: var(--border-w) solid rgb(var(--edge));
+ background: var(--bg-2);
+ box-shadow: var(--hard-x) var(--hard-y) 0 0 var(--hard-shadow);
+}
+
+.auth-copy {
+ min-height: 580px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ padding: clamp(2rem, 5vw, 5rem);
+ border-right: var(--border-w) solid rgb(var(--edge));
+}
+
+.auth-kicker {
+ margin-bottom: clamp(3rem, 10vh, 7rem);
+ font-family: var(--mono);
+ font-size: 0.72rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.auth-copy h1 {
+ max-width: 8ch;
+ margin: 0;
+ font-family: var(--display);
+ font-size: clamp(3.1rem, 6vw, 6rem);
+ font-weight: 760;
+ letter-spacing: -0.065em;
+ line-height: 0.9;
+}
+
+.auth-copy > p {
+ max-width: 42ch;
+ margin: 1.75rem 0 0;
+ color: var(--ink-dim);
+ font-family: var(--body);
+ font-size: 1rem;
+}
+
+.auth-notes {
+ width: 100%;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1.5rem;
+ margin: auto 0 0;
+ padding-top: 2rem;
+ border-top: 1px solid rgba(var(--edge), 0.28);
+}
+
+.auth-notes div {
+ min-width: 0;
+}
+
+.auth-notes dt,
+.auth-notes dd {
+ margin: 0;
+ font-family: var(--mono);
+ font-size: 0.7rem;
+}
+
+.auth-notes dt {
+ margin-bottom: 0.45rem;
+ color: var(--ink-dim);
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.auth-notes dd {
+ overflow-wrap: anywhere;
+}
+
+.auth-widget-shell {
+ display: grid;
+ place-items: center;
+ min-width: 0;
+ padding: clamp(1.5rem, 4vw, 3.5rem);
+ background: color-mix(in srgb, var(--accent) 9%, var(--bg));
+}
+
+.structured-auth-root,
+.structured-auth-card-box {
+ width: 100%;
+}
+
+.structured-auth-card {
+ width: 100%;
+ border: var(--border-w) solid rgb(var(--edge));
+ border-radius: 0;
+ box-shadow: 5px 5px 0 0 var(--hard-shadow);
+}
+
+.structured-auth-title {
+ font-weight: 760;
+ letter-spacing: -0.035em;
+}
+
+.structured-auth-subtitle {
+ color: var(--ink-dim);
+}
+
+.structured-auth-social,
+.structured-auth-primary,
+.structured-auth-input {
+ min-height: 44px;
+ border: 2px solid rgb(var(--edge));
+ border-radius: 0;
+ box-shadow: none;
+}
+
+.structured-auth-social:hover,
+.structured-auth-primary:hover {
+ transform: translate(1px, 1px);
+}
+
+.structured-auth-social:focus-visible,
+.structured-auth-primary:focus-visible,
+.structured-auth-input:focus-visible,
+.structured-auth-link:focus-visible {
+ outline: 3px solid var(--accent);
+ outline-offset: 2px;
+}
+
+.structured-auth-link {
+ color: #5f3fc4;
+ font-weight: 700;
+}
+
+.structured-auth-divider {
+ background: rgba(var(--edge), 0.3);
+}
+
+@media (max-width: 760px) {
+ .auth-page {
+ place-items: start stretch;
+ padding: 0;
+ }
+
+ .auth-frame {
+ grid-template-columns: 1fr;
+ border-right: 0;
+ border-left: 0;
+ box-shadow: none;
+ }
+
+ .auth-copy {
+ min-height: auto;
+ padding: 2rem 1.25rem 1.75rem;
+ border-right: 0;
+ border-bottom: var(--border-w) solid rgb(var(--edge));
+ }
+
+ .auth-kicker {
+ margin-bottom: 3rem;
+ }
+
+ .auth-copy h1 {
+ max-width: 9ch;
+ font-size: clamp(3rem, 16vw, 4.6rem);
+ }
+
+ .auth-notes {
+ margin-top: 3rem;
+ }
+
+ .auth-widget-shell {
+ padding: 1.25rem;
+ }
+}
diff --git a/artifacts/structured-liquidity/src/main.tsx b/artifacts/structured-liquidity/src/main.tsx
index c1f2205..3367ed1 100644
--- a/artifacts/structured-liquidity/src/main.tsx
+++ b/artifacts/structured-liquidity/src/main.tsx
@@ -1,13 +1,26 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
+import { ClerkProvider } from "@clerk/react";
import "./index.css";
import { App } from "./App";
const rootEl = document.getElementById("root");
if (!rootEl) throw new Error("Root element #root not found");
-createRoot(rootEl).render(
-
+const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY?.trim();
+
+const app = clerkPublishableKey ? (
+
- ,
+
+) : (
+
);
+
+createRoot(rootEl).render({app} );
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 48691ad..572f986 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -317,6 +317,10 @@ importers:
version: 3.25.76
artifacts/structured-liquidity:
+ dependencies:
+ '@clerk/react':
+ specifier: ^6.14.5
+ version: 6.14.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
devDependencies:
'@hookform/resolvers':
specifier: ^3.10.0
@@ -653,6 +657,13 @@ packages:
peerDependencies:
express: ^4.17.0 || ^5.0.0
+ '@clerk/react@6.14.5':
+ resolution: {integrity: sha512-J8VYDmlRqe2+fq49+FtsbQHJG6p5TcCjJ5myA0sBZ5ht8m2HsgJ9OV6qmIpupLmnDgu9Cp/9Pe/JpUM1JAxAjA==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
+ react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0
+
'@clerk/shared@4.29.3':
resolution: {integrity: sha512-sc9CNFDR3q+tJ8N5MkGcHGiXlgupQ6DMcZlXdk88kfo1Pl7G9EFK8e4mzp5D9OiYGRJ7rPnZCKiWoyInD59QXw==}
engines: {node: '>=20.9.0'}
@@ -4099,7 +4110,7 @@ snapshots:
'@clerk/backend@3.16.10':
dependencies:
- '@clerk/shared': 4.29.3
+ '@clerk/shared': 4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
standardwebhooks: 1.0.0
tslib: 2.8.1
transitivePeerDependencies:
@@ -4109,19 +4120,29 @@ snapshots:
'@clerk/express@2.1.61(express@5.2.1)':
dependencies:
'@clerk/backend': 3.16.10
- '@clerk/shared': 4.29.3
+ '@clerk/shared': 4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
express: 5.2.1
tslib: 2.8.1
transitivePeerDependencies:
- react
- react-dom
- '@clerk/shared@4.29.3':
+ '@clerk/react@6.14.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@clerk/shared': 4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ tslib: 2.8.1
+
+ '@clerk/shared@4.29.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@tanstack/query-core': 5.100.9
dequal: 2.0.3
glob-to-regexp: 0.4.1
js-cookie: 3.0.7
+ optionalDependencies:
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
'@commander-js/extra-typings@14.0.0(commander@14.0.3)':
dependencies:
From 7c0c215599ef11e98df02bef3ace0d1f84fe1959 Mon Sep 17 00:00:00 2001
From: Samir
Date: Sun, 23 Aug 2026 22:54:53 -0400
Subject: [PATCH 5/6] feat(auth): keep invitations on-domain
Prompt: Clerk sign-ins and invitations should be embedded on each application domain rather than starting on a random Clerk-hosted URL.
---
.../src/middleware/staging-access.ts | 2 +
artifacts/structured-liquidity/src/App.tsx | 4 +-
.../src/components/site/AuthPage.tsx | 66 +++++++++++++++----
artifacts/structured-liquidity/src/main.tsx | 2 +-
4 files changed, 61 insertions(+), 13 deletions(-)
diff --git a/artifacts/api-server/src/middleware/staging-access.ts b/artifacts/api-server/src/middleware/staging-access.ts
index 965c989..ae41674 100644
--- a/artifacts/api-server/src/middleware/staging-access.ts
+++ b/artifacts/api-server/src/middleware/staging-access.ts
@@ -110,6 +110,8 @@ function isPublicAuthSurfaceRequest(req: Request): boolean {
return (
req.path === "/sign-in" ||
req.path.startsWith("/sign-in/") ||
+ req.path === "/sign-up" ||
+ req.path.startsWith("/sign-up/") ||
req.path.startsWith("/assets/") ||
req.path === "/favicon.svg" ||
req.path === "/favicon.ico"
diff --git a/artifacts/structured-liquidity/src/App.tsx b/artifacts/structured-liquidity/src/App.tsx
index 32ba3bf..92ee9db 100644
--- a/artifacts/structured-liquidity/src/App.tsx
+++ b/artifacts/structured-liquidity/src/App.tsx
@@ -16,7 +16,9 @@ import { AuthPage } from "@/components/site/AuthPage";
export function App() {
if (
window.location.pathname === "/sign-in" ||
- window.location.pathname.startsWith("/sign-in/")
+ window.location.pathname.startsWith("/sign-in/") ||
+ window.location.pathname === "/sign-up" ||
+ window.location.pathname.startsWith("/sign-up/")
) {
return ;
}
diff --git a/artifacts/structured-liquidity/src/components/site/AuthPage.tsx b/artifacts/structured-liquidity/src/components/site/AuthPage.tsx
index 8f2e132..6ca9606 100644
--- a/artifacts/structured-liquidity/src/components/site/AuthPage.tsx
+++ b/artifacts/structured-liquidity/src/components/site/AuthPage.tsx
@@ -1,8 +1,12 @@
-import { SignIn } from "@clerk/react";
+import { SignIn, SignUp } from "@clerk/react";
const clerkPublishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY?.trim();
export function AuthPage() {
+ const isSignUp =
+ window.location.pathname === "/sign-up" ||
+ window.location.pathname.startsWith("/sign-up/");
+
if (!clerkPublishableKey) {
return (
@@ -22,7 +26,9 @@ export function AuthPage() {
Structured · Staging
-
Enter the system.
+
+ {isSignUp ? "Accept your invitation." : "Enter the system."}
+
Private review access for the Structured Liquidity design system.
@@ -39,13 +45,50 @@ export function AuthPage() {
-
+ ) : (
+
+ }}
+ />
+ )}
diff --git a/artifacts/structured-liquidity/src/main.tsx b/artifacts/structured-liquidity/src/main.tsx
index 3367ed1..b091c02 100644
--- a/artifacts/structured-liquidity/src/main.tsx
+++ b/artifacts/structured-liquidity/src/main.tsx
@@ -13,7 +13,7 @@ const app = clerkPublishableKey ? (
From 40d22bf6201a9d968b557c924ad4f03aea005473 Mon Sep 17 00:00:00 2001
From: Samir
Date: Mon, 24 Aug 2026 09:57:50 -0400
Subject: [PATCH 6/6] refine gallery field and responsive density
Prompt: Add a subtle background grid, make gallery Category and Type selectors visually distinct from the background, and use one, two, or three gallery columns for mobile, laptop, and wide screens.
---
.../src/styles/10-tokens-base.css | 9 ++++++---
.../src/styles/40-inline.css | 18 ++++++++++++------
2 files changed, 18 insertions(+), 9 deletions(-)
diff --git a/artifacts/structured-liquidity/src/styles/10-tokens-base.css b/artifacts/structured-liquidity/src/styles/10-tokens-base.css
index 08bce7e..b4b266a 100644
--- a/artifacts/structured-liquidity/src/styles/10-tokens-base.css
+++ b/artifacts/structured-liquidity/src/styles/10-tokens-base.css
@@ -32,6 +32,7 @@
--rule-hairline: 1px;
--diagram-line: color-mix(in srgb, var(--ink) 72%, transparent);
--diagram-muted: color-mix(in srgb, var(--ink) 42%, transparent);
+ --grid-line: rgba(17, 17, 17, 0.065);
/* ---- light-first paper palette ---- */
--bg: #f2f0ea;
@@ -61,6 +62,7 @@
--neg-ink: #ffffff;
--glass-tint: 255 255 255;
--glass-alpha: 0.72;
+ --grid-line: rgba(17, 17, 17, 0.065);
}
[data-mode="dark"] {
@@ -76,6 +78,7 @@
--neg-ink: #ffffff;
--glass-tint: 255 255 255;
--glass-alpha: 0.08;
+ --grid-line: rgba(246, 242, 248, 0.07);
}
* {
@@ -131,10 +134,10 @@ body {
position: absolute;
inset: 0;
background-image:
- linear-gradient(rgba(var(--edge), 0.045) 1px, transparent 1px),
- linear-gradient(90deg, rgba(var(--edge), 0.045) 1px, transparent 1px);
+ linear-gradient(var(--grid-line) 1px, transparent 1px),
+ linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 64px 64px;
- opacity: 0.7;
+ opacity: 1;
}
.grain {
diff --git a/artifacts/structured-liquidity/src/styles/40-inline.css b/artifacts/structured-liquidity/src/styles/40-inline.css
index 8adf3e4..2632a52 100644
--- a/artifacts/structured-liquidity/src/styles/40-inline.css
+++ b/artifacts/structured-liquidity/src/styles/40-inline.css
@@ -389,21 +389,23 @@
.kit-filter:hover{color:var(--ink);transform:translate(-1px,-1px);box-shadow:4px 4px 0 0 var(--hard-shadow);}
.kit-filter.active{background:var(--accent);color:var(--accent-ink);border-color:var(--accent);}
.kit-filter.active:hover{color:var(--accent-ink);}
- /* Masonry columns: uniform-width cards pack tight vertically (no ragged empty
- space under short cards) and get a clear horizontal gap between columns. */
+ /* Masonry columns: one on mobile, two on ordinary laptop viewports, and three
+ only once a wide display can preserve useful specimen width. */
.kit-grid.gallery{display:block;column-count:2;column-gap:1.4rem;}
.kit-grid.gallery .kit-cell{width:100%;margin:0 0 1.4rem;padding:1.5rem 1.6rem;
-webkit-column-break-inside:avoid;break-inside:avoid;}
+ @media (min-width:1440px){.kit-grid.gallery{column-count:3;}}
@media (max-width:600px){.kit-grid.gallery{column-count:1;}}
.kit-cell.is-hidden{display:none;}
- /* Two-axis gallery filters: a primary "Type" row + a secondary "Kind" row */
+ /* Two useful axes: Category describes the job; Type describes implementation
+ scope (primitive, composite, pattern, or template). Type stays subordinate. */
.kit-filterbars{display:flex;flex-direction:column;gap:0.6rem;margin:0.2rem 0 1.5rem;}
.kit-filter-row{display:flex;align-items:baseline;gap:0.7rem;flex-wrap:wrap;}
.kit-filter-axis{font-family:var(--mono);font-size:0.62rem;letter-spacing:0.16em;text-transform:uppercase;color:var(--ink);min-width:3rem;flex:0 0 auto;padding-top:0.55rem;display:flex;align-items:center;gap:0.45rem;}
.kit-filter-axis::before{content:"";width:0.5rem;height:0.5rem;background:var(--accent);flex:0 0 auto;}
.kit-filterbars .kit-filters{margin:0;flex:1 1 auto;}
- /* The secondary "Kind" axis is deliberately subordinate to "Type": a hollow
+ /* The secondary Type axis is deliberately subordinate to Category: a hollow
axis marker, smaller/flatter chips, and an accent-OUTLINE active state (vs
the primary row's solid accent fill). Distinct hierarchy, one accent. */
.kit-filter-row.is-secondary{padding-left:0.1rem;}
@@ -1502,6 +1504,9 @@ body {
column-count: 2;
column-gap: 1px;
}
+@media (min-width: 1440px) {
+ #components .kit-grid.gallery { column-count: 3; }
+}
#components .kit-grid.gallery .kit-cell {
display: flex;
flex-direction: column;
@@ -1534,8 +1539,9 @@ body {
}
.kit-filters[data-axis="cat"] .kit-filter.active:hover { color: var(--accent-ink); }
.kit-filters[data-axis="kind"] .kit-filter {
- background: color-mix(in srgb, var(--ink) 4%, transparent);
- border-color: color-mix(in srgb, var(--ink) 34%, transparent);
+ background: color-mix(in srgb, var(--ink) 6%, var(--bg-2));
+ border-color: color-mix(in srgb, var(--ink) 42%, transparent);
+ box-shadow: 1px 1px 0 0 color-mix(in srgb, var(--hard-shadow) 70%, transparent);
}
[data-mode="dark"] .kit-filters[data-axis="cat"] .kit-filter.active {
background: color-mix(in srgb, var(--accent) 54%, var(--bg-2));