diff --git a/apps/web/src/app.css b/apps/web/src/app.css
index 1d66c81..bba85e1 100644
--- a/apps/web/src/app.css
+++ b/apps/web/src/app.css
@@ -9,6 +9,14 @@
@source '../node_modules/@atmo-dev/events-ui/dist';
@import '@foxui/core/theme.css';
+/* Blento semantic design tokens. Each Tailwind theme value reads an overridable `--blento-*` CSS
+ variable with a default that matches today's look, so components use normal utilities (e.g.
+ `rounded-card`) that resolve through page-overridable tokens (set on by ThemeScript from
+ page.style). Colors stay on the @foxui accent/base mechanism above. */
+@theme inline {
+ --radius-card: var(--blento-radius-card, 1.5rem);
+}
+
@custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *)));
@custom-variant accent (&:where(.accent, .accent *));
diff --git a/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte b/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte
index bddd479..59fcbcc 100644
--- a/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte
+++ b/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte
@@ -39,7 +39,7 @@
bind:this={ref}
draggable={false}
class={[
- 'card group/card selection:bg-accent-600/50 @container/card relative isolate z-0 h-full w-full rounded-3xl outline-offset-2 transition-[outline] duration-200',
+ 'card group/card selection:bg-accent-600/50 rounded-card @container/card relative isolate z-0 h-full w-full outline-offset-2 transition-[outline] duration-200',
isEditing ? 'transition-all' : '',
color ? (colors[color] ?? colors.accent) : colors.base,
color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '',
@@ -50,7 +50,7 @@
>
` CSS-var pairs for ThemeScript. Colors stay
+ * on the @foxui accent/base class mechanism; radius/space/font flow through the token vars (e.g.
+ * `style.radius.card` -> `radius-card` -> `--blento-radius-card`). Empty when no page tokens are set.
+ */
+export function getStyleTokens(data: WebsiteData): Record
{
+ const style = data.publication?.style;
+ if (!style) return {};
+ const out: Record = {};
+ for (const group of ['radius', 'space', 'font'] as const) {
+ const g = style[group];
+ if (!g) continue;
+ for (const [k, v] of Object.entries(g)) out[`${group}-${k}`] = String(v);
+ }
+ return out;
+}
+
export function getName(data: WebsiteData): string {
return data.publication?.name || data.profile.displayName || data.handle;
}
diff --git a/apps/web/src/lib/types.ts b/apps/web/src/lib/types.ts
index cd20c0d..4d0e42c 100644
--- a/apps/web/src/lib/types.ts
+++ b/apps/web/src/lib/types.ts
@@ -1,6 +1,6 @@
import type { Blob } from '@atcute/lexicons';
import type { AppBskyActorDefs } from '@atcute/bluesky';
-import type { Node } from '@blento/schema';
+import type { Node, StyleTokens } from '@blento/schema';
export type Item = {
id: string;
@@ -91,6 +91,9 @@ export type WebsiteData = {
// explicit layout sync mode (overrides editedOn when set)
layoutMode?: 'desktop-leads' | 'mobile-leads' | 'independent';
};
+
+ /** Semantic design tokens (radius/space/font) applied as --blento-* CSS vars by ThemeScript. */
+ style?: StyleTokens;
};
profile: AppBskyActorDefs.ProfileViewDetailed;
pronouns?: string;
diff --git a/apps/web/src/lib/website/view/Head.svelte b/apps/web/src/lib/website/view/Head.svelte
index 909edbc..8548005 100644
--- a/apps/web/src/lib/website/view/Head.svelte
+++ b/apps/web/src/lib/website/view/Head.svelte
@@ -7,7 +7,8 @@
image,
description,
accentColor,
- baseColor
+ baseColor,
+ tokens
}: {
favicon: string | null;
title: string | null;
@@ -15,10 +16,11 @@
description?: string;
accentColor?: string;
baseColor?: string;
+ tokens?: Record;
} = $props();
-
+
{#if favicon}
diff --git a/apps/web/src/lib/website/view/ThemeScript.svelte b/apps/web/src/lib/website/view/ThemeScript.svelte
index 04922f5..47b2234 100644
--- a/apps/web/src/lib/website/view/ThemeScript.svelte
+++ b/apps/web/src/lib/website/view/ThemeScript.svelte
@@ -3,10 +3,14 @@
let {
accentColor = 'pink',
- baseColor = 'stone'
+ baseColor = 'stone',
+ tokens = {}
}: {
accentColor?: string;
baseColor?: string;
+ /** Page-level design tokens, applied as `--blento-` CSS vars on . e.g.
+ * { 'radius-card': '2rem' }. Empty = every token uses its default (identical to today). */
+ tokens?: Record;
} = $props();
const allAccentColors = [
@@ -32,11 +36,17 @@
const safeJson = (v: string) => JSON.stringify(v).replace(/(function(){document.documentElement.classList.add(${safeJson(accentColor)},${safeJson(baseColor)});})();<` +
+ // SSR: inline script for initial page load (no FOUC) — set color classes + token vars before paint.
+ // setProperty contains each value, so a token value can't break out of its declaration.
+ let script = $derived.by(() => {
+ const setProps = Object.entries(tokens)
+ .map(([k, v]) => `e.style.setProperty(${safeJson('--blento-' + k)},${safeJson(v)});`)
+ .join('');
+ return (
+ `
diff --git a/apps/web/src/lib/website/view/Website.svelte b/apps/web/src/lib/website/view/Website.svelte
index f2c5abd..b0e910e 100644
--- a/apps/web/src/lib/website/view/Website.svelte
+++ b/apps/web/src/lib/website/view/Website.svelte
@@ -5,7 +5,8 @@
getDescription,
getHideProfileSection,
getProfilePosition,
- getName
+ getName,
+ getStyleTokens
} from '$lib/helpers/website';
import { innerWidth } from 'svelte/reactivity/window';
import { setDidContext, setHandleContext, setIsMobile } from '../data/context';
@@ -59,6 +60,7 @@
description={getDescription(data)}
accentColor={data.publication?.preferences?.accentColor}
baseColor={data.publication?.preferences?.baseColor}
+ tokens={getStyleTokens(data)}
/>
--
2.51.2
From 559261a07b82d8fff9dab0dfaf9d44d40e7a3f6d Mon Sep 17 00:00:00 2001
From: Florian <45694132+flo-bit@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:48:42 +0200
Subject: [PATCH 2/5] =?UTF-8?q?theming:=20dev-only=20keyboard=20theme=20sw?=
=?UTF-8?q?itcher=20(press=201=E2=80=936)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A dev tool to eyeball the token system across theme presets without saving. Press
1–6 on any site page to flip accent/base colors + --blento-* tokens (radius) live;
reload restores the page's real theme. Rendered only in dev ({#if dev}), so it never
ships. 6 presets (Default/Ocean/Sunset/Forest/Grape/Rose) vary color + card radius —
demonstrating the page.style token pipeline end-to-end.
svelte-check 0/0.
---
.../lib/website/view/DevThemeSwitcher.svelte | 83 +++++++++++++++++++
apps/web/src/lib/website/view/Website.svelte | 6 ++
2 files changed, 89 insertions(+)
create mode 100644 apps/web/src/lib/website/view/DevThemeSwitcher.svelte
diff --git a/apps/web/src/lib/website/view/DevThemeSwitcher.svelte b/apps/web/src/lib/website/view/DevThemeSwitcher.svelte
new file mode 100644
index 0000000..7ed0a7b
--- /dev/null
+++ b/apps/web/src/lib/website/view/DevThemeSwitcher.svelte
@@ -0,0 +1,83 @@
+
+
+
+ {#if current >= 0}
+ {current + 1}. {presets[current].name}
+ ·
+ {/if}
+ theme 1–{presets.length}
+
diff --git a/apps/web/src/lib/website/view/Website.svelte b/apps/web/src/lib/website/view/Website.svelte
index b0e910e..9015244 100644
--- a/apps/web/src/lib/website/view/Website.svelte
+++ b/apps/web/src/lib/website/view/Website.svelte
@@ -22,6 +22,8 @@
import { user } from '$lib/atproto';
import { env } from '$env/dynamic/public';
import { page } from '$app/state';
+ import { dev } from '$app/environment';
+ import DevThemeSwitcher from './DevThemeSwitcher.svelte';
let { data }: { data: WebsiteData } = $props();
@@ -102,4 +104,8 @@
+
+ {#if dev}
+
+ {/if}
--
2.51.2
From cda2c6d17c013db4045960079fc54046391e3fab Mon Sep 17 00:00:00 2001
From: Florian <45694132+flo-bit@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:55:08 +0200
Subject: [PATCH 3/5] theming: add shadow + font tokens; enrich switcher
presets
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two more token dimensions, same pattern (default = today → 0px):
- --blento-shadow-card on the card wrapper via [box-shadow:var(...,0 0 #0000)] —
default invisible, so no change until a theme sets it (elevation).
- --blento-font-body on body via font-family: var(...,inherit) — default inherits the
current stack; a theme swaps typography.
- getStyleTokens now flattens the `shadow` group too.
- DevThemeSwitcher presets (Ocean/Sunset/Forest/Grape/Rose) now vary color + radius +
shadow + font, so the presets are genuinely distinct themes.
Validated: defaults pixel 0px on cards (only the dev indicator differs, 120px); forced
shadow+font override = 0.6% visible change (elevation + serif). svelte-check 0/0.
---
apps/web/src/app.css | 5 +++
.../lib/cards/_base/BaseCard/BaseCard.svelte | 2 +-
apps/web/src/lib/helpers/website.ts | 2 +-
.../lib/website/view/DevThemeSwitcher.svelte | 45 ++++++++++++++++---
4 files changed, 46 insertions(+), 8 deletions(-)
diff --git a/apps/web/src/app.css b/apps/web/src/app.css
index bba85e1..56e4c79 100644
--- a/apps/web/src/app.css
+++ b/apps/web/src/app.css
@@ -17,6 +17,11 @@
--radius-card: var(--blento-radius-card, 1.5rem);
}
+/* Body font token — defaults to `inherit` (the current stack), overridden per page/theme. */
+body {
+ font-family: var(--blento-font-body, inherit);
+}
+
@custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *)));
@custom-variant accent (&:where(.accent, .accent *));
diff --git a/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte b/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte
index 59fcbcc..8026009 100644
--- a/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte
+++ b/apps/web/src/lib/cards/_base/BaseCard/BaseCard.svelte
@@ -39,7 +39,7 @@
bind:this={ref}
draggable={false}
class={[
- 'card group/card selection:bg-accent-600/50 rounded-card @container/card relative isolate z-0 h-full w-full outline-offset-2 transition-[outline] duration-200',
+ 'card group/card selection:bg-accent-600/50 rounded-card @container/card relative isolate z-0 h-full w-full [box-shadow:var(--blento-shadow-card,0_0_#0000)] outline-offset-2 transition-[outline] duration-200',
isEditing ? 'transition-all' : '',
color ? (colors[color] ?? colors.accent) : colors.base,
color !== 'accent' && item.color !== 'base' && item.color !== 'transparent' ? color : '',
diff --git a/apps/web/src/lib/helpers/website.ts b/apps/web/src/lib/helpers/website.ts
index e568eba..4c6ff8b 100644
--- a/apps/web/src/lib/helpers/website.ts
+++ b/apps/web/src/lib/helpers/website.ts
@@ -9,7 +9,7 @@ export function getStyleTokens(data: WebsiteData): Record {
const style = data.publication?.style;
if (!style) return {};
const out: Record = {};
- for (const group of ['radius', 'space', 'font'] as const) {
+ for (const group of ['radius', 'shadow', 'space', 'font'] as const) {
const g = style[group];
if (!g) continue;
for (const [k, v] of Object.entries(g)) out[`${group}-${k}`] = String(v);
diff --git a/apps/web/src/lib/website/view/DevThemeSwitcher.svelte b/apps/web/src/lib/website/view/DevThemeSwitcher.svelte
index 7ed0a7b..6234c65 100644
--- a/apps/web/src/lib/website/view/DevThemeSwitcher.svelte
+++ b/apps/web/src/lib/website/view/DevThemeSwitcher.svelte
@@ -14,11 +14,44 @@
const presets: Preset[] = [
{ name: 'Default', accent: 'pink', base: 'stone', tokens: { 'radius-card': '1.5rem' } },
- { name: 'Ocean', accent: 'sky', base: 'slate', tokens: { 'radius-card': '0.5rem' } },
- { name: 'Sunset', accent: 'orange', base: 'neutral', tokens: { 'radius-card': '2rem' } },
- { name: 'Forest', accent: 'emerald', base: 'stone', tokens: { 'radius-card': '1rem' } },
- { name: 'Grape', accent: 'violet', base: 'zinc', tokens: { 'radius-card': '0.25rem' } },
- { name: 'Rose', accent: 'rose', base: 'gray', tokens: { 'radius-card': '9999px' } }
+ {
+ name: 'Ocean',
+ accent: 'sky',
+ base: 'slate',
+ tokens: { 'radius-card': '0.5rem', 'shadow-card': '0 8px 30px -8px rgb(2 20 40 / 0.25)' }
+ },
+ {
+ name: 'Sunset',
+ accent: 'orange',
+ base: 'neutral',
+ tokens: {
+ 'radius-card': '2rem',
+ 'shadow-card': '0 10px 40px -12px rgb(120 40 0 / 0.3)',
+ 'font-body': "Georgia, 'Times New Roman', serif"
+ }
+ },
+ {
+ name: 'Forest',
+ accent: 'emerald',
+ base: 'stone',
+ tokens: { 'radius-card': '1rem', 'shadow-card': '0 4px 16px -6px rgb(0 40 20 / 0.25)' }
+ },
+ {
+ name: 'Grape',
+ accent: 'violet',
+ base: 'zinc',
+ tokens: {
+ 'radius-card': '0.25rem',
+ 'shadow-card': '0 2px 0 2px rgb(80 40 120 / 0.2)',
+ 'font-body': "ui-monospace, 'SF Mono', 'Cascadia Code', monospace"
+ }
+ },
+ {
+ name: 'Rose',
+ accent: 'rose',
+ base: 'gray',
+ tokens: { 'radius-card': '9999px', 'shadow-card': '0 12px 32px -10px rgb(120 20 60 / 0.3)' }
+ }
];
const allAccent = [
@@ -41,7 +74,7 @@
'rose'
];
const allBase = ['gray', 'stone', 'zinc', 'neutral', 'slate'];
- const tokenKeys = ['radius-card'];
+ const tokenKeys = ['radius-card', 'shadow-card', 'font-body'];
let current = $state(-1);
--
2.51.2
From b287dae2d6a4766cd44c4d46f7f8706cd40f00eb Mon Sep 17 00:00:00 2001
From: Florian <45694132+flo-bit@users.noreply.github.com>
Date: Wed, 1 Jul 2026 17:22:55 +0200
Subject: [PATCH 4/5] theming: apply radius token beyond BaseCard (hero
decorations, section chrome)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The hero section renders its own decoration cards outside BaseCard (hardcoded
rounded-3xl), so they ignored the radius token. Swept card-level rounded-3xl ->
rounded-card across sections (HeroSection Decoration, section drop-zones, SectionChrome)
so all card-level corners follow --blento-radius-card. Default (1.5rem) == old rounded-3xl
so it's identical; a theme's radius now reaches the hero too.
(Media-card rounded-2xl embeds are clipped by BaseCard's overflow-hidden, so not visible
— left as-is.) Gate 0px; svelte-check 0/0.
---
.../sections/ColumnsSection/EditingColumnsSection.svelte | 2 +-
.../src/lib/sections/GridSection/EditingGridSection.svelte | 2 +-
apps/web/src/lib/sections/HeroSection/Decoration.svelte | 6 +++---
.../src/lib/sections/RowsSection/EditingRowsSection.svelte | 2 +-
apps/web/src/lib/sections/SectionChrome.svelte | 2 +-
.../SingleCardSection/EditingSingleCardSection.svelte | 2 +-
6 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte b/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte
index dd2c872..dd81550 100644
--- a/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte
+++ b/apps/web/src/lib/sections/ColumnsSection/EditingColumnsSection.svelte
@@ -84,7 +84,7 @@