diff --git a/docs/design/design-system-manifest-2026-10.md b/docs/design/design-system-manifest-2026-10.md
index aa2e62b9d..13c700f2c 100644
--- a/docs/design/design-system-manifest-2026-10.md
+++ b/docs/design/design-system-manifest-2026-10.md
@@ -123,6 +123,8 @@ remote insert is noise (the reference editor also inserts blocks with no entranc
dwell 2.8s (:3606), skeleton shimmer 1.6s (:442). They keep their durations but must use
an `--ease-*` token instead of `ease-out`/`ease`. Adding a new one requires a row here.
+Select exit: **none** — nested dismissable layer.
+
### 2.3 Reduced motion
| Rule | Decision |
diff --git a/eslint-rules/design-token-utils.mjs b/eslint-rules/design-token-utils.mjs
new file mode 100644
index 000000000..5a8ebc232
--- /dev/null
+++ b/eslint-rules/design-token-utils.mjs
@@ -0,0 +1,33 @@
+// Match class-bearing JSX and hoisted style/class constants, as spacing does.
+export function classContext(node) {
+ for (let current = node.parent; current; current = current.parent) {
+ if (current.type === 'JSXAttribute') return current.name.name === 'className';
+ if (['JSXElement', 'JSXFragment'].includes(current.type)) return false;
+ if (current.type === 'VariableDeclarator' && /class|style|motion/i.test(current.id.name ?? '')) return true;
+ if (current.type === 'Property' && /class|style/i.test(current.key.name ?? current.key.value ?? '')) return true;
+ }
+ return false;
+}
+export function classRule(description, rejects, extra = () => ({})) {
+ return {
+ meta: {type: 'suggestion', docs: {description}, schema: [], messages: {token: '{{value}} must use a named design token.'}},
+ create(context) {
+ const report = (node, value) => context.report({node, messageId: 'token', data: {value}});
+ function check(node, value) {
+ if (!classContext(node)) return;
+ for (const utility of value.split(/\s+/)) if (rejects(utility, context.filename)) report(node, utility);
+ }
+ const additional = extra(report);
+ return {
+ ...additional,
+ Literal(node) {
+ if (typeof node.value === 'string') check(node, node.value);
+ additional.Literal?.(node);
+ },
+ TemplateLiteral(node) {
+ check(node, node.quasis.map(q => q.value.cooked ?? q.value.raw).join('${}'));
+ },
+ };
+ },
+ };
+}
diff --git a/eslint-rules/design-tokens.test.mjs b/eslint-rules/design-tokens.test.mjs
new file mode 100644
index 000000000..8dfcb7e22
--- /dev/null
+++ b/eslint-rules/design-tokens.test.mjs
@@ -0,0 +1,26 @@
+import {RuleTester} from 'eslint';
+import {noArbitraryMotion} from './no-arbitrary-motion.mjs';
+import {noPaletteColor} from './no-palette-color.mjs';
+import {noRawZ} from './no-raw-z.mjs';
+const tester = new RuleTester({languageOptions: {ecmaVersion: 2022, sourceType: 'module', parserOptions: {ecmaFeatures: {jsx: true}}}});
+const jsx = value => `
`;
+const invalid = values => values.map(value => ({code: jsx(value), errors: [{messageId: 'token'}]}));
+tester.run('no-arbitrary-motion', noArbitraryMotion, {
+ valid: [jsx('duration-0 active:duration-0!'), jsx('duration-(--motion-base) ease-out-soft transition-colors'),
+ {code: jsx('ease-spring'), filename: '/ui/switch.tsx'},
+ {code: jsx('zoom-in-95'), filename: '/ui/overlay-motion.ts'},
+ 'const text = "duration-200";'],
+ invalid: [...invalid(['hover:duration-200', 'duration-[180ms]', 'delay-100', 'delay-[1s]', 'ease-in-out', 'ease-linear', 'ease-[cubic-bezier(0,0,1,1)]', 'animate-[foo_1s]', 'zoom-out-95', 'ease-spring']),
+ {code: 'const menuClass = `duration-200 ${active}`;', errors: [{messageId: 'token'}]}],
+});
+tester.run('no-palette-color', noPaletteColor, {
+ valid: [jsx('bg-muted text-muted-foreground bg-[var(--data-red)] text-[hsl(var(--foreground))]'), '', ''],
+ invalid: [...invalid(['dark:bg-red-500/20', 'text-slate-100', 'border-t-blue-300', 'bg-[#fff]', 'text-[rgb(0,0,0)]', 'bg-[hsl(0_0%_0%)]']),
+ {code: '', errors: [{messageId: 'token'}]},
+ {code: '', errors: [{messageId: 'token'}]}],
+});
+tester.run('no-raw-z', noRawZ, {
+ valid: [jsx('z-0 hover:z-0!'), jsx('z-menu hover:z-sticky'), 'const label = "z-50";'],
+ invalid: [...invalid(['z-50', '-z-10', 'hover:z-[1]', 'z-[var(--x)]']),
+ {code: 'const menuStyles = "z-10";', errors: [{messageId: 'token'}]}],
+});
diff --git a/eslint-rules/no-arbitrary-motion.mjs b/eslint-rules/no-arbitrary-motion.mjs
new file mode 100644
index 000000000..232bb2785
--- /dev/null
+++ b/eslint-rules/no-arbitrary-motion.mjs
@@ -0,0 +1,8 @@
+import {classRule} from './design-token-utils.mjs';
+export const noArbitraryMotion = classRule('Use the DSX motion scale.', (value, filename) => {
+ if (/(?:^|:)duration-0!?$/.test(value)) return false;
+ if (/(?:^|:)ease-spring!?$/.test(value) && !/(?:^|\/)switch\.tsx$/.test(filename)) return true;
+ if (/(?:^|:)zoom-(?:in|out)-95!?$/.test(value)) return !filename.endsWith('/overlay-motion.ts');
+ return /(?:^|:)(?:(?:duration|delay)-(?:\d+|\[[^\]]+\])|ease-(?:in|out|in-out|linear|\[[^\]]+\])|animate-\[[^\]]+\])!?$/.test(value);
+});
+export default {rules: {'no-arbitrary-motion': noArbitraryMotion}};
diff --git a/eslint-rules/no-palette-color.mjs b/eslint-rules/no-palette-color.mjs
new file mode 100644
index 000000000..c21b2fca9
--- /dev/null
+++ b/eslint-rules/no-palette-color.mjs
@@ -0,0 +1,19 @@
+import {classRule} from './design-token-utils.mjs';
+const colorKey = /^(?:color|background(?:Color)?|border(?:Top|Right|Bottom|Left)?(?:Color)?|outline(?:Color)?|fill|stroke|boxShadow|caretColor|textDecorationColor|accentColor)$/;
+const literal = /#[\da-f]{3,8}\b|\b(?:rgba?|hsla?)\(\s*[\d.]/i;
+export const noPaletteColor = classRule('Use semantic editor or data colours.', value =>
+ /(?:^|:)(?:bg|text|border(?:-[trblxyse])?|ring(?:-offset)?|fill|stroke|outline|from|via|to|decoration|caret|accent)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}(?:\/[^\s]+)?!?$/.test(value)
+ || /(?:^|:)(?:bg|text|border(?:-[trblxyse])?|ring|fill|stroke|outline|from|via|to|decoration|caret|accent)-\[/.test(value) && literal.test(value), report => ({
+ Literal(node) {
+ if (typeof node.value !== 'string' || !literal.test(node.value)) return;
+ const property = node.parent;
+ if (property.type !== 'Property' || !colorKey.test(property.key.name ?? property.key.value ?? '')) return;
+ for (let parent = property.parent; parent; parent = parent.parent) {
+ if (parent.type === 'JSXAttribute') {
+ if (parent.name.name === 'style') report(node, node.value);
+ return;
+ }
+ }
+ },
+}));
+export default {rules: {'no-palette-color': noPaletteColor}};
diff --git a/eslint-rules/no-raw-z.mjs b/eslint-rules/no-raw-z.mjs
new file mode 100644
index 000000000..5657c7163
--- /dev/null
+++ b/eslint-rules/no-raw-z.mjs
@@ -0,0 +1,3 @@
+import {classRule} from './design-token-utils.mjs';
+export const noRawZ = classRule('Use a named stacking tier.', value => !/(?:^|:)z-0!?$/.test(value) && /(?:^|:)-?z-(?:\d+|\[[^\]]+\])!?$/.test(value));
+export default {rules: {'no-raw-z': noRawZ}};
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 234fb9873..0978d0e1d 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -7,6 +7,9 @@ import nextPlugin from '@next/eslint-plugin-next';
import globals from 'globals';
import e2eIsolation from './eslint-rules/e2e-workspace-isolation.mjs';
import noArbitrarySpacing from './eslint-rules/no-arbitrary-spacing.mjs';
+import noArbitraryMotion from './eslint-rules/no-arbitrary-motion.mjs';
+import noPaletteColor from './eslint-rules/no-palette-color.mjs';
+import noRawZ from './eslint-rules/no-raw-z.mjs';
import noHoverGeometry from './eslint-rules/no-hover-geometry.mjs';
export default tseslint.config(
@@ -25,8 +28,6 @@ export default tseslint.config(
'**/blob-report/**',
// Rust host + generated Tauri capability schemas.
'packages/app/src-tauri/**',
- // shadcn/ui primitives, kept verbatim from upstream.
- 'packages/ui/src/components/ui/**',
// Vendored UMD bundles (d3 / Observable Plot) inlined into the HTML export.
'packages/ui/src/export/vendor/**',
// Generated mirror of the ledger plugin's PURE report folds (LX-3) —
@@ -54,16 +55,28 @@ export default tseslint.config(
},
{
// Hover may repaint an element, but it must not alter geometry and shift
- // adjacent content. UI primitives remain covered by the upstream ignore.
+ // adjacent content.
plugins: {'layout-shift': noHoverGeometry},
rules: {'layout-shift/no-hover-geometry': 'error'},
},
{
// Product spacing follows Tailwind's shared scale; bracket-arbitrary values
// for padding, margin, and gaps would silently introduce one-off geometry.
- plugins: {tailwind: noArbitrarySpacing},
+ plugins: {tailwind: {rules: {...noArbitrarySpacing.rules, ...noArbitraryMotion.rules, ...noPaletteColor.rules, ...noRawZ.rules}}},
rules: {'tailwind/no-arbitrary-spacing': 'error'},
},
+ {
+ // DSX warn rollout: baseline 13 duration + 3 easing utilities, 208 palette
+ // utilities, 43 numeric z utilities; preserve existing debt without CI failure.
+ // Measured with these guards at 1d6ef346: motion 33→16, palette 152→152,
+ // z 40→40 (includes primitives; excludes generated/vendor sources).
+ files: ['packages/{ui,web}/src/**/*.{ts,tsx}'],
+ rules: {
+ 'tailwind/no-arbitrary-motion': 'warn',
+ 'tailwind/no-palette-color': 'warn',
+ 'tailwind/no-raw-z': 'warn',
+ },
+ },
{
// Plain Node scripts (build helpers, etc.).
files: ['**/*.{js,cjs,mjs}'],
@@ -109,4 +122,20 @@ export default tseslint.config(
plugins: {e2e: e2eIsolation},
rules: {'e2e/workspace-isolation': 'error'},
},
+ {
+ // Upstream primitives retain their formatting/type lint exemption, but
+ // DSX guards cover them too (including future overlay motion regressions).
+ files: ['packages/ui/src/components/ui/**/*.{ts,tsx}'],
+ rules: {
+ ...Object.fromEntries(Object.keys({
+ ...js.configs.recommended.rules,
+ ...Object.assign({}, ...tseslint.configs.recommended.map(config => config.rules)),
+ ...react.configs.flat.recommended.rules,
+ }).map(name => [name, 'off'])),
+ 'indent': 'off', 'linebreak-style': 'off', 'quotes': 'off', 'semi': 'off',
+ 'no-eval': 'off', 'no-new-func': 'off',
+ 'layout-shift/no-hover-geometry': 'off',
+ 'tailwind/no-arbitrary-spacing': 'off',
+ },
+ },
);
diff --git a/packages/ui/scripts/assert-spacing-stylelint.mjs b/packages/ui/scripts/assert-spacing-stylelint.mjs
index 66c69ad56..0b83c5785 100644
--- a/packages/ui/scripts/assert-spacing-stylelint.mjs
+++ b/packages/ui/scripts/assert-spacing-stylelint.mjs
@@ -19,7 +19,7 @@ async function assertAllowed(declaration) {
async function assertRejected(declaration) {
const warnings = await warningsFor(`.spc2-negative-control { ${declaration}; }`);
- if (!warnings.some((warning) => warning.rule === rule)) {
+ if (!warnings.some((warning) => warning.severity === 'error' && [rule, 'design-tokens/role-values'].includes(warning.rule))) {
throw new Error(`SPC-2 stylelint negative control did not reject ${declaration}`);
}
}
@@ -33,3 +33,35 @@ await assertRejected('border-radius: calc(var(--radius) + 3px)');
await assertRejected('PADDING: 7PX');
console.log('SPC-2 stylelint controls passed (2 positive, 4 negative)');
+
+// DSX guards: token declarations and semantic colours remain legal.
+for (const value of [
+ 'transition: color var(--motion-fast) var(--ease-out-soft)',
+ 'transition-duration: 0s', 'animation-timing-function: linear',
+ 'color: hsl(var(--foreground) / 0.6)', 'border-radius: var(--radius-lg)',
+ 'z-index: var(--z-index-menu)', 'font-size: var(--obe-small-size)',
+ 'font-size: .875em', 'font-size: 1em', '--motion-fast: 120ms', '--obe-code-num: hsl(28 80% 38%)',
+]) {
+ const warnings = await warningsFor(`.obe-control { ${value}; }`);
+ if (warnings.length) throw new Error(`DSX allowed ${value}: ${JSON.stringify(warnings)}`);
+}
+for (const value of [
+ 'transition: color 120ms ease', 'transition-duration: 0ms', 'animation: enter .2s ease-out',
+ 'transition-timing-function: cubic-bezier(0, 0, 1, 1)',
+ 'color: #abcdef', 'border: 1px solid rgb(1 2 3)',
+ 'font-size: 14px',
+]) {
+ const warnings = await warningsFor(`.obe-control { ${value}; }`);
+ if (!warnings.length || warnings.some(w => w.severity !== 'warning')) throw new Error(`DSX warn guard missed ${value}`);
+}
+for (const value of ['border-radius: 4px', 'border-radius: var(--anything)', 'z-index: 4']) {
+ const warnings = await warningsFor(`.obe-control { ${value}; }`);
+ if (!warnings.length || warnings.some(w => w.severity !== 'error')) throw new Error(`DSX error guard missed ${value}`);
+}
+const {readFileSync} = await import('node:fs');
+const css = readFileSync(codeFilename, 'utf8');
+const reducedBlocks = css.match(/@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\n\}/g) ?? [];
+if (reducedBlocks.length !== 2 || !reducedBlocks[0].includes('0.01ms !important') || !reducedBlocks[1].includes('.obe-rcursor-label { animation: none; opacity: 1; }')) {
+ throw new Error('Reduced motion must use only the global switch and remote cursor end-state exception');
+}
+console.log('DSX stylelint controls passed');
diff --git a/packages/ui/scripts/design-token-stylelint.mjs b/packages/ui/scripts/design-token-stylelint.mjs
new file mode 100644
index 000000000..7db0ed632
--- /dev/null
+++ b/packages/ui/scripts/design-token-stylelint.mjs
@@ -0,0 +1,19 @@
+import stylelint from 'stylelint';
+const ruleName = 'design-tokens/role-values';
+const radiusAtom = String.raw`(?:0|50%|9999px|var\(--(?:radius-(?:sm|md|lg|xl)|ob-page-radius)\))`;
+const radius = new RegExp(`^${radiusAtom}(?:\\s+${radiusAtom}){0,3}$`);
+const font = /^(?:var\(--obe-(?:font-size|[\w-]+-size|code-inline)\)|inherit|(?:0?(?:\.\d+)|0|1(?:\.0+)?)em)$/;
+const z = /^(?:var\(--z-index-[\w-]+\)|0|auto|-1)$/;
+export default stylelint.createPlugin(ruleName, enabled => (root, result) => {
+ if (!enabled) return;
+ root.walkDecls(decl => {
+ const prop = decl.prop.toLowerCase();
+ const pattern = prop === 'border-radius' ? radius : prop === 'z-index' ? z
+ : prop === 'font-size' && /\.obe-[\w-]+/.test(decl.parent.selector ?? '') ? font : null;
+ if (pattern && !pattern.test(decl.value)) stylelint.utils.report({
+ ruleName, result, node: decl,
+ severity: prop === 'font-size' ? 'warning' : 'error',
+ message: `Use a DSX role token for ${prop}: ${decl.value}`,
+ });
+ });
+});
diff --git a/packages/ui/src/blockeditor/__tests__/colors.test.ts b/packages/ui/src/blockeditor/__tests__/colors.test.ts
index 05599a2b9..368230e7e 100644
--- a/packages/ui/src/blockeditor/__tests__/colors.test.ts
+++ b/packages/ui/src/blockeditor/__tests__/colors.test.ts
@@ -22,10 +22,11 @@ function rgba(value: string): number[] {
return [channel(0), channel(8), channel(4), Number(match[4] ?? 1)];
}
-function literal(selector: string, property: string): string {
- const rule = css.split('\n').find((line) => line.startsWith(`${selector} {`));
- const value = rule?.match(new RegExp(`${property}: hsl\\(([^)]+)\\)`))?.[1];
- if (!value) throw new Error(`Missing ${selector} ${property}`);
+function literal(dark: boolean, role: typeof roles[number], id: string): string {
+ const theme = css.match(dark ? /\.dark\s*\{([^}]+)\}/ : /:root\s*\{([^}]+)\}/)?.[1];
+ const property = `--obe-${role}-${id}`;
+ const value = theme?.match(new RegExp(`${property}: hsl\\(([^)]+)\\)`))?.[1];
+ if (!value) throw new Error(`Missing ${dark ? '.dark' : ':root'} ${property}`);
return value;
}
@@ -64,12 +65,11 @@ for (const dark of [false, true]) {
expect(COLOR_TOKENS).toHaveLength(9);
});
for (const {id} of COLOR_TOKENS) {
- const selector = `${dark ? '.dark ' : ''}.obe-`;
it(`${id} text meets 4.5:1`, () => {
- expect(contrast(rgba(literal(`${selector}fg-${id}`, 'color')), rgba(background))).toBeGreaterThanOrEqual(4.5);
+ expect(contrast(rgba(literal(dark, 'fg', id)), rgba(background))).toBeGreaterThanOrEqual(4.5);
});
it.each(roles)(`${id} %s matches export hex`, (role) => {
- expect(palette[id][role]).toBe(hex(rgba(literal(`${selector}${role}-${id}`, role === 'fg' ? 'color' : 'background-color'))));
+ expect(palette[id][role]).toBe(hex(rgba(literal(dark, role, id))));
});
}
});
diff --git a/packages/ui/src/blockeditor/__tests__/gutterStyles.test.ts b/packages/ui/src/blockeditor/__tests__/gutterStyles.test.ts
index 1ce6c5b17..fe93753ae 100644
--- a/packages/ui/src/blockeditor/__tests__/gutterStyles.test.ts
+++ b/packages/ui/src/blockeditor/__tests__/gutterStyles.test.ts
@@ -115,7 +115,7 @@ describe('column resize styles', () => {
it('keeps a raised, touch-safe one-rem hit zone with a two-pixel hover rule', () => {
const divider = ruleBody('.obe-col-divider');
- expect(divider).toMatch(/z-index:\s*4/);
+ expect(divider).toMatch(/z-index:\s*var\(--z-index-pane-overlay\)/);
expect(divider).toMatch(/width:\s*1rem/);
expect(divider).toMatch(/touch-action:\s*none/);
expect(ruleBody('.obe-col-divider::after')).toMatch(/width:\s*2px/);
diff --git a/packages/ui/src/components/ui/context-menu.tsx b/packages/ui/src/components/ui/context-menu.tsx
index 9c20cf239..d4327cb71 100644
--- a/packages/ui/src/components/ui/context-menu.tsx
+++ b/packages/ui/src/components/ui/context-menu.tsx
@@ -1,3 +1,4 @@
+import { OVERLAY_MOTION } from "./overlay-motion"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { CheckIcon, ChevronRightIcon } from "@radix-ui/react-icons"
@@ -49,7 +50,8 @@ const ContextMenuSubContent = React.forwardRef<
,
DialogContentProps
@@ -58,7 +60,8 @@ const DialogContent = React.forwardRef<
className={cn(
// `min-w-0 [&>*]:min-w-0` removes the panel/direct-child auto minimums so long children cannot widen it past max-w-*; deep content still needs min-w-0/truncate/break-*.
// The body may scroll; floating content is portaled so it is not clipped. A future direct child's min-w-* loses to `[&>*]:min-w-0` by utility order, not intent.
- "relative grid max-h-[calc(100vh-4rem)] min-w-0 w-full gap-4 overflow-y-auto rounded-lg border bg-background p-6 shadow-overlay duration-[var(--default-transition-duration)] ease-[var(--ease-out-soft)] [&>*]:min-w-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
+ "relative grid max-h-[calc(100vh-4rem)] min-w-0 w-full gap-4 overflow-y-auto rounded-xl border bg-background p-6 shadow-overlay [&>*]:min-w-0",
+ OVERLAY_MOTION.dialog,
dialogContentSizes[size],
className
)}
diff --git a/packages/ui/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx
index 2d7a8b399..9a0b2f4d6 100644
--- a/packages/ui/src/components/ui/dropdown-menu.tsx
+++ b/packages/ui/src/components/ui/dropdown-menu.tsx
@@ -1,3 +1,4 @@
+import { OVERLAY_MOTION } from "./overlay-motion"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import {
@@ -57,7 +58,8 @@ const DropdownMenuSubContent = React.forwardRef<
ref={ref}
collisionPadding={collisionPadding}
className={cn(
- "z-50 min-w-32 overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:pointer-events-none data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ "z-50 min-w-32 overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-overlay",
+ OVERLAY_MOTION.menu,
density === "compact" ? "p-0.5" : "p-1",
className
)}
@@ -84,8 +86,8 @@ const DropdownMenuContent = React.forwardRef<
// the space below the trigger — the unified page menu (CM-2) can run
// past the fold, and overflow-hidden would strand its last items (e.g.
// "Move to trash") off-screen and unclickable.
- "z-50 max-h-[min(var(--radix-dropdown-menu-content-available-height),calc(100vh-20px))] min-w-32 overflow-y-auto overflow-x-hidden overscroll-contain rounded-md border bg-popover text-popover-foreground shadow-overlay",
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:pointer-events-none data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ "z-50 max-h-[min(var(--radix-dropdown-menu-content-available-height),calc(100vh-20px))] min-w-32 overflow-y-auto overflow-x-hidden overscroll-contain rounded-lg border bg-popover text-popover-foreground shadow-overlay",
+ OVERLAY_MOTION.menu,
density === "compact" ? "p-0.5" : "p-1",
className
)}
diff --git a/packages/ui/src/components/ui/overlay-motion.ts b/packages/ui/src/components/ui/overlay-motion.ts
new file mode 100644
index 000000000..f23d0bf3e
--- /dev/null
+++ b/packages/ui/src/components/ui/overlay-motion.ts
@@ -0,0 +1,9 @@
+// DSX §2: exits fade only; menus enter 2px from their anchor at scale .98.
+const fade = 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=closed]:pointer-events-none data-[state=closed]:duration-(--motion-fast) data-[state=open]:ease-out-soft data-[state=closed]:ease-in-out-soft';
+export const OVERLAY_MOTION = {
+ menuEnter: 'data-[state=open]:animate-in fade-in-0 duration-(--motion-base) ease-out-soft zoom-in-[0.98] data-[side=bottom]:slide-in-from-top-0.5 data-[side=left]:slide-in-from-right-0.5 data-[side=right]:slide-in-from-left-0.5 data-[side=top]:slide-in-from-bottom-0.5',
+ menu: `${fade} data-[state=open]:duration-(--motion-base) data-[state=open]:zoom-in-[0.98] data-[state=open]:data-[side=bottom]:slide-in-from-top-0.5 data-[state=open]:data-[side=left]:slide-in-from-right-0.5 data-[state=open]:data-[side=right]:slide-in-from-left-0.5 data-[state=open]:data-[side=top]:slide-in-from-bottom-0.5`,
+ dialog: `${fade} data-[state=open]:duration-(--motion-slow) data-[state=open]:zoom-in-[0.98]`,
+ scrim: `${fade} data-[state=open]:duration-(--motion-slow)`,
+ tooltip: `${fade} data-[state=open]:duration-(--motion-fast)`,
+};
diff --git a/packages/ui/src/components/ui/popover.tsx b/packages/ui/src/components/ui/popover.tsx
index 397eff074..3aec88906 100644
--- a/packages/ui/src/components/ui/popover.tsx
+++ b/packages/ui/src/components/ui/popover.tsx
@@ -1,3 +1,4 @@
+import { OVERLAY_MOTION } from "./overlay-motion"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
@@ -24,7 +25,8 @@ const PopoverContent = React.forwardRef<
// these vars when avoidCollisions is on — the default) and scroll when
// the content is taller than the viewport, so a popover opened near an
// edge never spills off-screen.
- "z-50 max-h-[var(--radix-popover-content-available-height)] w-72 max-w-[calc(100vw-1rem)] overflow-y-auto overflow-x-hidden overscroll-contain rounded-md border bg-popover p-4 text-popover-foreground shadow-overlay outline-hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ "z-50 max-h-[var(--radix-popover-content-available-height)] w-72 max-w-[calc(100vw-1rem)] overflow-y-auto overflow-x-hidden overscroll-contain rounded-lg border bg-popover p-4 text-popover-foreground shadow-overlay outline-hidden",
+ OVERLAY_MOTION.menu,
className
)}
{...props}
diff --git a/packages/ui/src/components/ui/select.tsx b/packages/ui/src/components/ui/select.tsx
index 31a9466f4..c026f0d1d 100644
--- a/packages/ui/src/components/ui/select.tsx
+++ b/packages/ui/src/components/ui/select.tsx
@@ -1,3 +1,4 @@
+import { OVERLAY_MOTION } from "./overlay-motion"
import * as React from "react"
import {Check, ChevronDown} from "lucide-react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
@@ -198,11 +199,9 @@ const Select = React.forwardRef(function Select(
onKeyDown={onKeyDown}
className={cn(
"z-50 max-h-[var(--radix-popover-content-available-height)] min-w-[var(--radix-popover-trigger-width)] max-w-[calc(100vw-1rem)]",
- "overflow-y-auto overflow-x-hidden overscroll-contain rounded-md border bg-popover p-1 text-popover-foreground shadow-overlay outline-hidden",
- // Entrance only — no exit animation, so the content (and its dismissable
- // layer) unmounts immediately on close rather than lingering and
- // interfering with an enclosing popover.
- "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
+ "overflow-y-auto overflow-x-hidden overscroll-contain rounded-lg border bg-popover p-1 text-popover-foreground shadow-overlay outline-hidden",
+ // Entrance only — no exit animation (nested dismissable layer).
+ OVERLAY_MOTION.menuEnter,
)}
>
{items.length === 0 && (
diff --git a/packages/ui/src/components/ui/switch.tsx b/packages/ui/src/components/ui/switch.tsx
index f84b5382e..ae50c267c 100644
--- a/packages/ui/src/components/ui/switch.tsx
+++ b/packages/ui/src/components/ui/switch.tsx
@@ -9,7 +9,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
diff --git a/packages/ui/src/components/ui/tooltip.tsx b/packages/ui/src/components/ui/tooltip.tsx
index 922dff6bf..94d0db191 100644
--- a/packages/ui/src/components/ui/tooltip.tsx
+++ b/packages/ui/src/components/ui/tooltip.tsx
@@ -1,3 +1,4 @@
+import { OVERLAY_MOTION } from "./overlay-motion"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
@@ -18,7 +19,8 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-lift animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+ "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-lift",
+ OVERLAY_MOTION.tooltip,
className
)}
{...props}
diff --git a/packages/ui/src/export/__tests__/export.test.ts b/packages/ui/src/export/__tests__/export.test.ts
index 841fba4b9..d898063f2 100644
--- a/packages/ui/src/export/__tests__/export.test.ts
+++ b/packages/ui/src/export/__tests__/export.test.ts
@@ -148,18 +148,23 @@ describe('toMarkdown', () => {
describe('stored callout variants', () => {
it.each([
- ['info', 'rgba(59,130,246,.10)', '💡'],
- ['warn', 'rgba(245,158,11,.12)', '⚠️'],
- ['success', 'rgba(34,197,94,.12)', '✅'],
- ['danger', 'rgba(239,68,68,.12)', '🛑'],
+ ['info', 'hsl(40 9% 96%)', '💡'],
+ ['warn', 'hsl(45 90% 52% / 0.15)', '⚠️'],
+ ['warning', 'hsl(45 90% 52% / 0.15)', '⚠️'],
+ ['success', 'hsl(140 55% 45% / 0.13)', '✅'],
+ ['danger', 'hsl(0 72% 55% / 0.12)', '🛑'],
])('exports %s with its matching tint and icon', (variant, tint, icon) => {
const blocks: BlockJSON[] = [{id: 'callout', type: 'callout', text: [{t: 'Notice'}], props: {variant}}];
const html = toHtml(snapshot(projectBlocksForExport(blocks).blocks), 'T', '');
expect(html).toContain(`data-variant="${variant}"`);
const selector = variant === 'info' ? '.callout' : `.callout[data-variant=${variant}]`;
- expect(html).toContain(`${selector} { background: ${tint};`);
- expect(html).toContain(`${selector}::before { content: "${icon}"; }`);
- expect(html).toContain('.callout[data-variant=warning], .callout[data-variant=warn] {');
+ const tintSelector = variant === 'warn' || variant === 'warning'
+ ? '.callout[data-variant=warn], .callout[data-variant=warning]' : selector;
+ expect(html).toContain(`${tintSelector} { background: ${tint};`);
+ const iconSelector = variant === 'warn' || variant === 'warning'
+ ? '.callout[data-variant=warn]::before, .callout[data-variant=warning]::before' : `${selector}::before`;
+ expect(html).toContain(`${iconSelector} { content: "${icon}"; }`);
+ expect(html).toContain('.callout[data-variant=warn], .callout[data-variant=warning] {');
});
});
diff --git a/packages/ui/src/export/__tests__/foundationTokens.test.ts b/packages/ui/src/export/__tests__/foundationTokens.test.ts
new file mode 100644
index 000000000..b2df1548d
--- /dev/null
+++ b/packages/ui/src/export/__tests__/foundationTokens.test.ts
@@ -0,0 +1,58 @@
+import {describe, expect, it} from 'vitest';
+// @ts-expect-error -- node:fs is available to Vitest, not the browser package.
+import {readFileSync} from 'node:fs';
+
+const css: string = readFileSync('src/index.css', 'utf8');
+const exporter: string = readFileSync('src/export/toHtml.ts', 'utf8');
+const root = css.slice(css.indexOf(' :root {'), css.indexOf(' .dark {'));
+const dark = css.slice(css.indexOf(' .dark {'), css.indexOf('\n html,'));
+function darkToken(name: string): string {
+ const match = dark.match(new RegExp(`--${name}: ([^;]+);`));
+ expect(match, name).not.toBeNull();
+ return match![1];
+}
+function token(name: string): string {
+ const match = root.match(new RegExp(`--${name}: ([^;]+);`));
+ expect(match, name).not.toBeNull();
+ return match![1];
+}
+
+describe('DSX foundation export mirrors', () => {
+ it('keeps the static type ramp equal to the plain CSS tokens', () => {
+ const body = parseFloat(token('obe-font-size'));
+ expect(exporter).toContain(`font: ${body}px/${token('obe-leading')} `);
+ for (const [name, selector] of [['h1', 'h1'], ['h2', 'h2'], ['h3', 'h3'], ['title', 'h1.doc-title']] as const) {
+ const ratio = Number(token(`obe-${name}-size`).match(/\* ([\d.]+)/)![1]);
+ expect(exporter).toContain(`${selector} { font-size: ${body * ratio}px;`);
+ if (name !== 'title') expect(exporter).toContain(`margin-top: ${token(`obe-${name}-space`)};`);
+ }
+ expect(exporter).toContain(`font-weight: ${token('obe-heading-weight')}; line-height: ${token('obe-h1-leading')};`);
+ expect(exporter).toContain(`font-family: ${token('obe-font-mono')}; font-size: ${token('obe-code-inline')};`);
+ const codeRatio = Number(token('obe-code-size').match(/\* ([\d.]+)/)![1]);
+ expect(exporter).toContain(`pre code { font-size: ${body * codeRatio}px; line-height: ${token('obe-code-leading')}; }`);
+ });
+
+ it('mirrors borderless callout tints and supports authored warn plus legacy warning', () => {
+ expect(exporter).toContain(`.callout { background: hsl(${token('muted')}); }`);
+ const dual = exporter.slice(exporter.indexOf('const SCHEME_DUAL ='), exporter.indexOf('const stylesFor ='));
+ expect(dual).toContain(`.callout { background: hsl(${darkToken('muted')}); }`);
+ for (const [variant, hue] of [['warn', 'yellow'], ['success', 'green'], ['danger', 'red']]) {
+ expect(token(`obe-callout-${variant}`)).toBe(`var(--obe-bg-${hue})`);
+ const selector = variant === 'warn' ? '.callout[data-variant=warn], .callout[data-variant=warning]' : `.callout[data-variant=${variant}]`;
+ expect(exporter).toContain(`${selector} { background: ${token(`obe-bg-${hue}`)}; }`);
+ }
+ expect(exporter).toContain('.callout[data-variant=warn], .callout[data-variant=warning]');
+ expect(exporter).toContain('padding: 12px; border-radius: 8px; border: none; }');
+ });
+
+ it('declares every palette role and syntax hue in both modes', () => {
+ for (const role of ['fg', 'bg', 'hl']) {
+ for (const hue of ['gray', 'brown', 'orange', 'yellow', 'green', 'blue', 'purple', 'pink', 'red']) {
+ expect(token(`obe-${role}-${hue}`)).toMatch(/^hsl\(/);
+ expect(dark).toContain(`--obe-${role}-${hue}: hsl(`);
+ }
+ }
+ for (const hue of ['kw', 'str', 'num', 'lit']) expect(dark).toContain(`--obe-code-${hue}: hsl(`);
+ expect(token('obe-code-num')).toBe('hsl(28 80% 38%)');
+ });
+});
diff --git a/packages/ui/src/export/toHtml.ts b/packages/ui/src/export/toHtml.ts
index 4c030fb75..f88a1129f 100644
--- a/packages/ui/src/export/toHtml.ts
+++ b/packages/ui/src/export/toHtml.ts
@@ -1255,6 +1255,7 @@ const SCHEME_DUAL = `
:root { color-scheme: light dark; }
@media (prefers-color-scheme: dark) {
body { background: #18181b; color: #e7e7ea; }
+ .callout { background: hsl(0 0% 18.5%); }
/* Brighter text-colour tokens so palette colours stay legible on the dark page
(the light-theme hex go muddy). Inline runs reference these via var(); when
this query is inactive the var() falls back to the baked light hex. */
@@ -1270,19 +1271,24 @@ const SCHEME_DUAL = `
const stylesFor = (scheme: DataColorScheme): string => `
* { box-sizing: border-box; }
-body { margin: 0; background: #fff; color: #1a1a1a; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
+body { margin: 0; background: #fff; color: #1a1a1a; font: 16px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
main { max-width: 720px; margin: 0 auto; padding: 48px 24px 120px; }
section.page[hidden] { display: none; }
.ob-nav { position: sticky; top: 0; z-index: 10; padding: 8px 24px; backdrop-filter: blur(8px); background: rgba(127,127,127,.06); border-bottom: 1px solid rgba(127,127,127,.18); }
.ob-nav button { font: inherit; font-size: .9rem; cursor: pointer; border: 1px solid rgba(127,127,127,.3); background: transparent; color: inherit; border-radius: 6px; padding: 4px 12px; }
.ob-nav button:hover { background: rgba(127,127,127,.12); }
-h1.doc-title { font-size: 2.4rem; font-weight: 800; letter-spacing: -.02em; margin: 0 0 1.2rem; }
-h1,h2,h3,h4 { font-weight: 700; line-height: 1.25; margin: 1.6em 0 .4em; }
+h1.doc-title { font-size: 40px; line-height: 1.2; font-weight: 700; letter-spacing: -.02em; margin: 0 0 1.2rem; }
+/* DSX §1 light mirrors; foundationTokens.test.ts checks equality with index.css. */
+h1,h2,h3,h4 { font-weight: 600; line-height: 1.3; margin: 0 0 .4em; }
+h1 { font-size: 30px; margin-top: 2rem; }
+h2 { font-size: 24px; margin-top: 1.5rem; }
+h3 { font-size: 20px; margin-top: 1rem; }
p { margin: .6em 0; }
ul,ol { margin: .4em 0; padding-left: 1.4em; }
blockquote { margin: 1em 0; padding: .2em 0 .2em 1em; border-left: 3px solid currentColor; opacity: .85; font-style: italic; }
pre { background: rgba(127,127,127,.12); padding: 12px 14px; border-radius: 8px; overflow-x: auto; }
-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; }
+code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.875em; }
+pre code { font-size: 14px; line-height: 1.5; }
/* Unstyled highlights keep an opaque tint and dark text. Translucent palette
highlights inherit the light page ink in dark mode via SCHEME_DUAL. */
mark { background: #fde68a; color: #1c1917; padding: 0 .1em; border-radius: 2px; }
@@ -1398,15 +1404,15 @@ table.db-table a.db-row { display: inline-flex; align-items: center; gap: 6px; c
table.db-table a.db-row:hover { text-decoration: underline; }
.db-empty { opacity: .6; font-size: .9em; }
.tag { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: .82em; margin: 1px 2px 1px 0; }
-.callout { display: flex; gap: 8px; margin: 8px 0; padding: 12px; border-radius: 8px; border: 1px solid; }
+.callout { display: flex; gap: 8px; margin: 8px 0; padding: 12px; border-radius: 8px; border: none; }
.callout::before { content: "💡"; }
-.callout[data-variant=warning]::before, .callout[data-variant=warn]::before { content: "⚠️"; }
+.callout[data-variant=warn]::before, .callout[data-variant=warning]::before { content: "⚠️"; }
.callout[data-variant=success]::before { content: "✅"; }
.callout[data-variant=danger]::before { content: "🛑"; }
-.callout { background: rgba(59,130,246,.10); border-color: rgba(59,130,246,.35); }
-.callout[data-variant=warning], .callout[data-variant=warn] { background: rgba(245,158,11,.12); border-color: rgba(245,158,11,.4); }
-.callout[data-variant=success] { background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.4); }
-.callout[data-variant=danger] { background: rgba(239,68,68,.12); border-color: rgba(239,68,68,.4); }
+.callout { background: hsl(40 9% 96%); }
+.callout[data-variant=warn], .callout[data-variant=warning] { background: hsl(45 90% 52% / 0.15); }
+.callout[data-variant=success] { background: hsl(140 55% 45% / 0.13); }
+.callout[data-variant=danger] { background: hsl(0 72% 55% / 0.12); }
.callout__body { flex: 1; }
.accordion { margin: 8px 0; border: 1px solid rgba(127,127,127,.25); border-radius: 8px; padding: 4px 12px; }
.accordion summary { cursor: pointer; font-weight: 600; padding: 4px 0; }
diff --git a/packages/ui/src/index.css b/packages/ui/src/index.css
index c77817fda..ec610c07d 100644
--- a/packages/ui/src/index.css
+++ b/packages/ui/src/index.css
@@ -108,12 +108,13 @@
--height-control-md: 32px;
--height-control-lg: 36px;
+ --radius-xl: calc(var(--radius) + 4px);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
- --animate-accordion-down: accordion-down 0.2s ease-out;
- --animate-accordion-up: accordion-up 0.2s ease-out;
+ --animate-accordion-down: accordion-down var(--motion-base) var(--ease-out-soft);
+ --animate-accordion-up: accordion-up var(--motion-base) var(--ease-out-soft);
--transition-property-height: height;
--transition-property-spacing: margin, padding;
@@ -152,7 +153,7 @@
touching call sites. Named eases are exposed as `ease-out-soft` / `ease-spring`
utilities for entrances and emphasis. Static end-states are unchanged, so
visual snapshots are unaffected; only the in-between motion is refined. */
- --default-transition-duration: 180ms;
+ --default-transition-duration: 120ms;
--default-transition-timing-function: cubic-bezier(0.32, 0.72, 0, 1);
--ease-out-soft: cubic-bezier(0.32, 0.72, 0, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
@@ -213,6 +214,95 @@
}
:root {
+ /* DSX foundation: plain CSS tokens also travel with the standalone viewer. */
+ --motion-fast: 120ms;
+ --motion-base: 180ms;
+ --motion-slow: 240ms;
+ --ease-out-soft: cubic-bezier(0.32, 0.72, 0, 1);
+ --ease-in-out-soft: cubic-bezier(0.65, 0, 0.35, 1);
+ --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --obe-font-size: 16px;
+ --obe-leading: 1.5;
+ --obe-line: calc(var(--obe-font-size) * var(--obe-leading));
+ --obe-title-size: calc(var(--obe-font-size) * 2.5);
+ --obe-title-leading: 1.2;
+ --obe-heading-weight: 600;
+ --obe-code-size: calc(var(--obe-font-size) * 0.875);
+ --obe-code-leading: 1.5;
+ --obe-code-inline: 0.875em;
+ --obe-small-size: calc(var(--obe-font-size) * 0.875);
+ --obe-small-leading: 1.43;
+ --obe-caption-size: calc(var(--obe-font-size) * 0.75);
+ --obe-caption-leading: 1.33;
+ --obe-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ --obe-placeholder: hsl(var(--muted-foreground) / 0.6);
+ --obe-callout-info: hsl(var(--muted));
+ --obe-callout-warn: var(--obe-bg-yellow);
+ --obe-callout-success: var(--obe-bg-green);
+ --obe-callout-danger: var(--obe-bg-red);
+ --obe-code-bg: hsl(var(--muted) / 0.6);
+ --obe-code-border: hsl(var(--border) / 0.7);
+ --obe-code-com: hsl(var(--muted-foreground));
+ --obe-h1-size: calc(var(--obe-font-size) * 1.875);
+ --obe-h1-leading: 1.3;
+ --obe-h1-space: 2rem;
+ --obe-h2-size: calc(var(--obe-font-size) * 1.5);
+ --obe-h2-leading: 1.3;
+ --obe-h2-space: 1.5rem;
+ --obe-h3-size: calc(var(--obe-font-size) * 1.25);
+ --obe-h3-leading: 1.3;
+ --obe-h3-space: 1rem;
+ --z-index-raised: 1;
+ --z-index-control: 2;
+ --z-index-drop-indicator: 3;
+ --z-index-pane-overlay: 4;
+ --z-index-local-overlay: 5;
+ --z-index-presence: 6;
+ --z-index-sticky: 10;
+ --z-index-inline-hint: 20;
+ --z-index-raised-chrome: 30;
+ --z-index-overlay: 40;
+ --z-index-drawer: 45;
+ --z-index-menu: 50;
+ --z-index-nested-menu: 55;
+ --z-index-lightbox: 60;
+ --z-index-toast: 100;
+ --obe-fg-gray: hsl(0 0% 45%);
+ --obe-fg-brown: hsl(25 35% 38%);
+ --obe-fg-orange: hsl(28 80% 39%);
+ --obe-fg-yellow: hsl(42 80% 33%);
+ --obe-fg-green: hsl(140 50% 34%);
+ --obe-fg-blue: hsl(210 72% 45%);
+ --obe-fg-purple: hsl(265 55% 52%);
+ --obe-fg-pink: hsl(330 65% 48%);
+ --obe-fg-red: hsl(0 68% 48%);
+ --obe-bg-gray: hsl(0 0% 50% / 0.1);
+ --obe-bg-brown: hsl(25 45% 50% / 0.13);
+ --obe-bg-orange: hsl(28 85% 52% / 0.13);
+ --obe-bg-yellow: hsl(45 90% 52% / 0.15);
+ --obe-bg-green: hsl(140 55% 45% / 0.13);
+ --obe-bg-blue: hsl(210 80% 55% / 0.13);
+ --obe-bg-purple: hsl(265 60% 58% / 0.14);
+ --obe-bg-pink: hsl(330 70% 58% / 0.13);
+ --obe-bg-red: hsl(0 72% 55% / 0.12);
+ --obe-hl-gray: hsl(0 0% 55% / 0.28);
+ --obe-hl-brown: hsl(25 50% 50% / 0.3);
+ --obe-hl-orange: hsl(30 90% 55% / 0.32);
+ --obe-hl-yellow: hsl(48 95% 55% / 0.4);
+ --obe-hl-green: hsl(140 60% 48% / 0.3);
+ --obe-hl-blue: hsl(210 85% 58% / 0.28);
+ --obe-hl-purple: hsl(265 65% 60% / 0.3);
+ --obe-hl-pink: hsl(330 75% 60% / 0.3);
+ --obe-hl-red: hsl(0 75% 58% / 0.28);
+ --obe-code-kw: hsl(265 55% 48%);
+ --obe-code-str: hsl(140 50% 30%);
+ --obe-code-num: hsl(28 80% 38%);
+ --obe-code-lit: hsl(210 72% 42%);
+
/* Warm, refined light palette */
--background: 0 0% 100%;
--foreground: 34 9% 19%;
@@ -300,6 +390,38 @@
}
.dark {
+ --obe-fg-gray: hsl(0 0% 66%);
+ --obe-fg-brown: hsl(25 40% 62%);
+ --obe-fg-orange: hsl(28 85% 62%);
+ --obe-fg-yellow: hsl(45 80% 62%);
+ --obe-fg-green: hsl(140 50% 60%);
+ --obe-fg-blue: hsl(210 75% 67%);
+ --obe-fg-purple: hsl(265 65% 73%);
+ --obe-fg-pink: hsl(330 70% 71%);
+ --obe-fg-red: hsl(0 72% 69%);
+ --obe-bg-gray: hsl(0 0% 78% / 0.1);
+ --obe-bg-brown: hsl(25 45% 55% / 0.18);
+ --obe-bg-orange: hsl(28 80% 55% / 0.18);
+ --obe-bg-yellow: hsl(45 80% 55% / 0.18);
+ --obe-bg-green: hsl(140 50% 50% / 0.18);
+ --obe-bg-blue: hsl(210 70% 55% / 0.2);
+ --obe-bg-purple: hsl(265 55% 60% / 0.22);
+ --obe-bg-pink: hsl(330 60% 58% / 0.2);
+ --obe-bg-red: hsl(0 65% 55% / 0.2);
+ --obe-hl-gray: hsl(0 0% 62% / 0.3);
+ --obe-hl-brown: hsl(25 45% 50% / 0.34);
+ --obe-hl-orange: hsl(30 80% 50% / 0.36);
+ --obe-hl-yellow: hsl(48 80% 50% / 0.34);
+ --obe-hl-green: hsl(140 50% 45% / 0.34);
+ --obe-hl-blue: hsl(210 70% 52% / 0.36);
+ --obe-hl-purple: hsl(265 55% 55% / 0.38);
+ --obe-hl-pink: hsl(330 60% 55% / 0.36);
+ --obe-hl-red: hsl(0 65% 52% / 0.36);
+ --obe-code-kw: hsl(265 65% 73%);
+ --obe-code-str: hsl(140 50% 60%);
+ --obe-code-num: hsl(28 85% 62%);
+ --obe-code-lit: hsl(210 75% 67%);
+
/* Refined dark palette: main surface dark, sidebar slightly lighter.
Softened 2026-06-13: lifted the base, eased the text, and calmed the
accent so dark mode reads less harsh (lower contrast, less glare). */
@@ -669,7 +791,7 @@
font-family: var(--ob-font-heading, var(--ob-font-body, inherit));
}
[data-placeholder]:empty::before {
- color: hsl(var(--muted-foreground) / 0.7);
+ color: var(--obe-placeholder);
font-style: normal;
}
/* A database host page (compact editor) shows its table right below the tiny
@@ -686,7 +808,7 @@
cursor: pointer;
white-space: nowrap;
padding: 0 4px;
- border-radius: calc(var(--radius) - 2px);
+ border-radius: var(--radius-sm);
color: inherit;
font-weight: 500;
text-decoration: underline;
@@ -697,237 +819,18 @@
background-color: hsl(var(--accent));
}
-/* ── Reactive blocks (slider / expr / chart) ────────────────────────────── */
-/* Give the interactive blocks a little breathing room within the document
- flow so they read as distinct cards rather than crowding adjacent text. */
-.reactive-block {
- margin: 0.25rem 0;
-}
/* @cell references inside an expression render as inline chips: monospace,
tinted with the brand color, non-editable as a single atomic token. */
.cell-token {
display: inline-block;
padding: 0 4px;
margin: 0;
- border-radius: calc(var(--radius) - 2px);
+ border-radius: var(--radius-sm);
background-color: hsl(var(--brand-subtle));
color: hsl(var(--brand));
font-weight: 600;
white-space: nowrap;
}
-/* Observable Plot legend swatches sit inline; keep their labels readable. */
-.reactive-block figure {
- margin: 0;
-}
-.reactive-block figure > div {
- color: hsl(var(--muted-foreground));
- font-size: 0.75rem;
-}
-
-/* ── New block types ──────────────────────────────────────────────────────── */
-
-/* Our custom contenteditable regions need an explicit placeholder — a bare
- contenteditable doesn't render data-placeholder text natively, so we supply
- the content here. */
-.block-callout__body[data-placeholder]:empty::before,
-.block-accordion__title[data-placeholder]:empty::before,
-.block-accordion__content[data-placeholder]:empty::before,
-.block-divider__label[data-placeholder]:empty::before {
- content: attr(data-placeholder);
- color: hsl(var(--muted-foreground) / 0.6);
- pointer-events: none;
-}
-
-/* Callout — a tinted box keyed off the variant hue. */
-.block-callout {
- --callout: 217 91% 60%; /* info */
- display: flex;
- gap: 0.5rem;
- margin: 0.5rem 0;
- padding: 0.75rem;
- border: 1px solid hsl(var(--callout) / 0.35);
- border-radius: var(--radius);
- background: hsl(var(--callout) / 0.08);
-}
-.block-callout[data-variant='warning'] { --callout: 38 92% 50%; }
-.block-callout[data-variant='success'] { --callout: 142 71% 45%; }
-.block-callout[data-variant='danger'] { --callout: 0 84% 60%; }
-.block-callout__emoji {
- flex: none;
- cursor: pointer;
- border: 0;
- background: transparent;
- font-size: 1.1rem;
- line-height: 1.6;
- padding: 0;
-}
-.block-callout__body {
- flex: 1;
- min-width: 0;
- line-height: 1.6;
- outline: none;
-}
-
-/* Accordion — collapsible summary + body with a rotating chevron. */
-.block-accordion {
- margin: 0.5rem 0;
- border: 1px solid hsl(var(--border));
- border-radius: var(--radius);
- padding: 0.25rem 0.75rem;
-}
-.block-accordion__header {
- display: flex;
- align-items: flex-start;
- gap: 0.5rem;
-}
-.block-accordion__chevron {
- flex: none;
- display: flex;
- cursor: pointer;
- border: 0;
- background: transparent;
- color: hsl(var(--muted-foreground));
- padding: 0.25rem 0;
- transition: transform 0.15s ease;
-}
-.block-accordion[data-open='true'] .block-accordion__chevron {
- transform: rotate(90deg);
-}
-.block-accordion__title {
- flex: 1;
- font-weight: 600;
- padding: 0.25rem 0;
- line-height: 1.5;
- outline: none;
-}
-.block-accordion__content {
- padding: 0 0 0.5rem 1.5rem;
- line-height: 1.6;
- outline: none;
-}
-.block-accordion[data-open='false'] .block-accordion__content {
- display: none;
-}
-
-/* Divider — a rule whose style is data-driven; `labeled` adds a centred caption. */
-.block-divider {
- display: flex;
- align-items: center;
- margin: 1rem 0;
-}
-.block-divider::before {
- content: '';
- flex: 1;
- border-top: 1px solid hsl(var(--border));
-}
-.block-divider[data-style='dashed']::before { border-top-style: dashed; }
-.block-divider[data-style='dotted']::before { border-top-style: dotted; }
-.block-divider[data-style='thick']::before { border-top-width: 3px; }
-.block-divider__label { display: none; }
-.block-divider[data-style='labeled'] { gap: 0.75rem; }
-.block-divider[data-style='labeled']::after {
- content: '';
- flex: 1;
- border-top: 1px solid hsl(var(--border));
-}
-.block-divider[data-style='labeled'] .block-divider__label {
- display: block;
- flex: none;
- min-width: 1ch;
- color: hsl(var(--muted-foreground));
- font-size: 0.8rem;
- text-align: center;
- outline: none;
-}
-
-/* Button / link card. */
-.block-button {
- margin: 0.5rem 0;
-}
-.block-button__fields {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-top: 0.5rem;
-}
-/* Once a URL is set, the document shows just the CTA — the label/URL fields
- fold away until "Edit button" (block settings) or until the URL is cleared. */
-.block-button--configured:not(.block-button--editing) .block-button__fields {
- display: none;
-}
-.block-button__input {
- flex: 1;
- min-width: 8rem;
- border: 1px solid hsl(var(--input));
- border-radius: calc(var(--radius) - 2px);
- background: transparent;
- color: hsl(var(--foreground));
- padding: 0.375rem 0.625rem;
- font-size: 0.85rem;
- outline: none;
-}
-.block-button__input:focus {
- border-color: hsl(var(--ring));
-}
-/* Doubled class bumps specificity so a CTA button never inherits
- link-underline styling and reads as a link. */
-.block-button__cta.block-button__cta {
- display: inline-block;
- background: hsl(var(--primary));
- color: hsl(var(--primary-foreground));
- padding: 0.5rem 1rem;
- border-radius: var(--radius);
- font-weight: 600;
- font-size: 0.9rem;
- text-decoration: none;
- cursor: pointer;
-}
-.block-button__cta.is-empty {
- background: hsl(var(--muted));
- color: hsl(var(--muted-foreground));
- cursor: default;
-}
-
-/* Table of contents. */
-.block-toc {
- margin: 0.5rem 0;
- padding: 0.5rem 0.75rem;
- border-left: 3px solid hsl(var(--border));
-}
-.block-toc__label {
- margin-bottom: 0.25rem;
- font-size: 0.7rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.04em;
- color: hsl(var(--muted-foreground));
-}
-.block-toc__empty {
- color: hsl(var(--muted-foreground));
- font-size: 0.85rem;
- font-style: italic;
-}
-.block-toc__list {
- list-style: none;
- margin: 0;
- padding: 0;
-}
-.block-toc__item {
- margin: 0;
-}
-.block-toc__link {
- border: 0;
- background: transparent;
- cursor: pointer;
- text-align: left;
- color: hsl(var(--muted-foreground));
- font-size: 0.875rem;
- padding: 0;
-}
-.block-toc__link:hover {
- color: hsl(var(--foreground));
- text-decoration: underline;
-}
/* ── Block editor (obe-*) ─────────────────────────────────────────────────────
The custom CRDT block editor. Plain classes (no Tailwind apply) so the
@@ -959,8 +862,8 @@
margin-inline: auto;
/* stylelint-disable-next-line declaration-property-value-allowed-list -- SPC-2: viewport-relative append zone; manifest section 4. */
padding: 0.25rem 0 30vh;
- font-size: 15px;
- line-height: 1.65;
+ font-size: var(--obe-font-size);
+ line-height: var(--obe-leading);
color: hsl(var(--foreground));
}
@@ -977,7 +880,7 @@
position: relative;
display: flex;
align-items: flex-start;
- border-radius: 8px;
+ border-radius: var(--radius-sm);
padding: 0;
}
.obe-row-selected {
@@ -993,7 +896,7 @@
pointer-events: none;
background: hsl(var(--ring) / 0.14);
border: 1px solid hsl(var(--ring) / 0.65);
- border-radius: 4px;
+ border-radius: var(--radius-sm);
}
.obe-marqueeing,
.obe-marqueeing * {
@@ -1009,7 +912,7 @@
font-weight: 600;
color: hsl(var(--primary-foreground));
background: hsl(var(--primary));
- border-radius: 6px;
+ border-radius: var(--radius-md);
box-shadow: 0 2px 8px hsl(var(--foreground) / 0.25);
white-space: nowrap;
}
@@ -1029,7 +932,7 @@
gap: 4px;
opacity: 0;
pointer-events: none;
- transition: opacity 120ms ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
/* Inside columns there's no page gutter — a compact, handle-only grip
floats in the column gap. Keep the visible grip above the resize divider;
@@ -1080,7 +983,7 @@
height: 1.5rem;
display: grid;
place-items: center;
- border-radius: 6px;
+ border-radius: var(--radius-md);
color: hsl(var(--muted-foreground) / 0.7);
font-size: 14px;
cursor: pointer;
@@ -1136,19 +1039,19 @@
}
.obe-text:empty::before {
content: attr(data-placeholder);
- color: hsl(var(--muted-foreground) / 0.45);
+ color: var(--obe-placeholder);
pointer-events: none;
}
-.obe-heading { flex: 1; min-width: 0; font-weight: 650; }
-.obe-h1 .obe-text { font-size: 1.7rem; line-height: 1.25; margin-top: 0.75rem; }
-.obe-h2 .obe-text { font-size: 1.35rem; line-height: 1.3; margin-top: 0.5rem; }
-.obe-h3 .obe-text { font-size: 1.12rem; line-height: 1.35; margin-top: 0.25rem; }
+.obe-heading { flex: 1; min-width: 0; font-weight: var(--obe-heading-weight); }
+.obe-h1 .obe-text { font-size: var(--obe-h1-size); line-height: var(--obe-h1-leading); margin-top: var(--obe-h1-space); }
+.obe-h2 .obe-text { font-size: var(--obe-h2-size); line-height: var(--obe-h2-leading); margin-top: var(--obe-h2-space); }
+.obe-h3 .obe-text { font-size: var(--obe-h3-size); line-height: var(--obe-h3-leading); margin-top: var(--obe-h3-space); }
.obe-code, code.obe-code {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 0.875em;
+ font-family: var(--obe-font-mono);
+ font-size: var(--obe-code-inline);
background: hsl(var(--muted) / 0.7);
- border-radius: 4px;
+ border-radius: var(--radius-sm);
padding: 0 4px;
color: hsl(var(--foreground));
}
@@ -1162,7 +1065,7 @@ a.obe-link {
a.obe-link:hover { text-decoration-color: hsl(var(--foreground)); }
a.obe-mention {
background: hsl(var(--accent) / 0.8);
- border-radius: 4px;
+ border-radius: var(--radius-sm);
padding: 0 4px;
text-decoration: none;
cursor: pointer;
@@ -1207,20 +1110,20 @@ a.obe-mention {
display: flex;
min-width: 0;
gap: 0.5rem;
- background: hsl(var(--muted) / 0.55);
- border-radius: 8px;
+ background: var(--obe-callout-info);
+ border-radius: var(--radius-lg);
padding: 0.75rem;
margin: 0;
}
-.obe-callout-warn { background: hsl(40 90% 55% / 0.12); }
-.obe-callout-success { background: hsl(140 60% 45% / 0.1); }
-.obe-callout-danger { background: hsl(0 75% 55% / 0.09); }
+.obe-callout-warn { background: var(--obe-callout-warn); }
+.obe-callout-success { background: var(--obe-callout-success); }
+.obe-callout-danger { background: var(--obe-callout-danger); }
.obe-callout-icon {
flex: none;
font-size: 1.05rem;
line-height: 1.5;
cursor: pointer;
- border-radius: 6px;
+ border-radius: var(--radius-md);
padding: 0;
}
.obe-callout-icon:hover { background: var(--hover); }
@@ -1237,7 +1140,7 @@ a.obe-mention {
min-width: 0;
border-left: 3px solid hsl(var(--primary) / 0.4);
background: hsl(var(--primary) / 0.05);
- border-radius: 0 8px 8px 0;
+ border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
padding: 0.5rem 0.75rem;
margin: 0;
color: hsl(var(--muted-foreground));
@@ -1273,13 +1176,13 @@ a.obe-mention {
position: relative;
display: inline-block;
max-width: 100%;
- border-radius: 8px;
+ border-radius: var(--radius-lg);
}
.obe-image-img {
display: block;
width: 100%;
height: auto;
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--muted) / 0.4);
-webkit-user-select: none;
user-select: none;
@@ -1293,12 +1196,12 @@ a.obe-mention {
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--background) / 0.86);
box-shadow: 0 1px 4px hsl(0 0% 0% / 0.16);
backdrop-filter: blur(6px);
opacity: 0;
- transition: opacity 0.12s ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.obe-image-frame:hover .obe-image-tools,
.obe-image-tools:focus-within { opacity: 1; }
@@ -1331,10 +1234,10 @@ a.obe-mention {
height: 44px;
max-height: 60%;
transform: translateY(-50%);
- border-radius: 6px;
+ border-radius: var(--radius-md);
cursor: ew-resize;
opacity: 0;
- transition: opacity 0.12s ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.obe-image-resize::after {
content: '';
@@ -1343,7 +1246,7 @@ a.obe-mention {
margin: auto;
width: 4px;
height: 100%;
- border-radius: 4px;
+ border-radius: var(--radius-sm);
background: hsl(var(--foreground) / 0.35);
box-shadow: 0 0 0 2px hsl(var(--background) / 0.7);
}
@@ -1360,7 +1263,7 @@ a.obe-mention {
}
.obe-image-alt { font-size: 0.75rem; opacity: 0.85; }
.obe-image-alt::placeholder,
-.obe-image-caption-input::placeholder { color: hsl(var(--muted-foreground) / 0.55); }
+.obe-image-caption-input::placeholder { color: var(--obe-placeholder); }
.obe-image-caption {
color: hsl(var(--muted-foreground));
font-size: 0.82rem;
@@ -1376,12 +1279,12 @@ a.obe-mention {
width: 100%;
padding: 1.5rem 1rem;
border: 1px dashed hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--muted) / 0.35);
color: hsl(var(--muted-foreground));
text-align: center;
cursor: pointer;
- transition: border-color 0.12s ease, background 0.12s ease;
+ transition: border-color var(--motion-fast) var(--ease-out-soft), background var(--motion-fast) var(--ease-out-soft);
}
.obe-image-placeholder:hover {
border-color: hsl(var(--primary) / 0.5);
@@ -1405,7 +1308,7 @@ a.obe-mention {
}
/* External image drag hovering the editor — subtle drop-target confirmation so
a drop reads as intentional (pairs with the drag/drop handlers). */
-.obe-root.obe-file-dragover { box-shadow: inset 0 0 0 2px hsl(var(--primary) / 0.4); border-radius: 8px; }
+.obe-root.obe-file-dragover { box-shadow: inset 0 0 0 2px hsl(var(--primary) / 0.4); border-radius: var(--radius-lg); }
.obe-root.obe-file-dragover .obe-image-empty .obe-image-placeholder,
.obe-root.obe-file-dragover .obe-artifact-empty .obe-image-placeholder {
border-color: hsl(var(--primary) / 0.7);
@@ -1458,7 +1361,7 @@ a.obe-mention {
max-height: 100%;
object-fit: contain;
transform-origin: center center;
- border-radius: 6px;
+ border-radius: var(--radius-xl);
box-shadow: 0 12px 48px hsl(0 0% 0% / 0.55);
will-change: transform;
user-select: none;
@@ -1503,7 +1406,7 @@ a.obe-mention {
color: hsl(0 0% 100% / 0.85);
background: transparent;
cursor: pointer;
- transition: background 0.12s ease, color 0.12s ease, opacity 0.12s ease;
+ transition: background var(--motion-fast) var(--ease-out-soft), color var(--motion-fast) var(--ease-out-soft), opacity var(--motion-fast) var(--ease-out-soft);
}
.obe-lightbox-zoombtn:hover:not(:disabled) {
background: hsl(0 0% 100% / 0.16);
@@ -1535,11 +1438,11 @@ a.obe-mention {
justify-content: center;
width: 2.25rem;
height: 2.25rem;
- border-radius: 8px;
+ border-radius: var(--radius-md);
color: hsl(0 0% 100% / 0.85);
background: hsl(0 0% 0% / 0.35);
cursor: pointer;
- transition: background 0.12s ease, color 0.12s ease;
+ transition: background var(--motion-fast) var(--ease-out-soft), color var(--motion-fast) var(--ease-out-soft);
}
.obe-lightbox-close:hover {
background: hsl(0 0% 100% / 0.16);
@@ -1582,7 +1485,7 @@ a.obe-mention {
font-size: 0.82rem;
font-weight: 500;
}
-.obe-artifact-title::placeholder { color: hsl(var(--muted-foreground) / 0.55); }
+.obe-artifact-title::placeholder { color: var(--obe-placeholder); }
.obe-artifact-title-static {
font-size: 0.82rem;
font-weight: 500;
@@ -1615,10 +1518,10 @@ a.obe-mention {
max-width: 60%;
height: 10px;
transform: translateX(-50%);
- border-radius: 6px;
+ border-radius: var(--radius-md);
cursor: ns-resize;
opacity: 0;
- transition: opacity 0.12s ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.obe-artifact-resize::after {
content: '';
@@ -1627,7 +1530,7 @@ a.obe-mention {
margin: auto;
width: 100%;
height: 4px;
- border-radius: 4px;
+ border-radius: var(--radius-sm);
background: hsl(var(--foreground) / 0.35);
box-shadow: 0 0 0 2px hsl(var(--background) / 0.7);
}
@@ -1644,13 +1547,13 @@ a.obe-mention {
align-items: center;
justify-content: center;
padding: 0.25rem;
- border-radius: 6px;
+ border-radius: var(--radius-md);
color: hsl(var(--muted-foreground));
background: hsl(var(--background) / 0.85);
box-shadow: 0 0 0 1px hsl(var(--border));
cursor: pointer;
opacity: 0;
- transition: opacity 0.12s ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.obe-artifact-expand:hover { background: var(--hover); color: hsl(var(--foreground)); }
/* Keyboard reveal + themed ring live in the shared focus-visible rule below. */
@@ -1702,11 +1605,23 @@ a.obe-mention {
width: 100%;
max-width: 56rem;
margin: auto 0; /* vertically centre short slides; tall ones scroll */
- animation: ob-slide-in 340ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
+ animation: ob-slide-in var(--motion-slow) var(--ease-out-soft) both;
+}
+.ob-present-full .ob-slide {
+ --obe-font-size: 1.08rem;
+ /* Recompute derived sizes at the scaling context (CSS variables resolve here). */
+ --obe-title-size: calc(var(--obe-font-size) * 2.5);
+ --obe-code-size: calc(var(--obe-font-size) * 0.875);
+ --obe-small-size: calc(var(--obe-font-size) * 0.875);
+ --obe-caption-size: calc(var(--obe-font-size) * 0.75);
+ --obe-h1-size: calc(var(--obe-font-size) * 1.875);
+ --obe-h2-size: calc(var(--obe-font-size) * 1.5);
+ --obe-h3-size: calc(var(--obe-font-size) * 1.25);
+ --obe-line: calc(var(--obe-font-size) * var(--obe-leading));
+ font-size: var(--obe-font-size);
}
-.ob-present-full .ob-slide { font-size: 1.08rem; }
@keyframes ob-slide-in {
- from { opacity: 0; transform: translateY(18px); }
+ from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: none; }
}
.ob-slide-title { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem; }
@@ -1758,7 +1673,7 @@ a.obe-mention {
right: 0.9rem;
z-index: var(--z-index-sticky);
opacity: 0;
- transition: opacity 160ms;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.ob-present-full:hover .ob-present-topbar,
.ob-present-full .ob-present-topbar:focus-within { opacity: 1; }
@@ -1770,11 +1685,11 @@ a.obe-mention {
align-items: center;
justify-content: center;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--background));
color: hsl(var(--muted-foreground));
cursor: pointer;
- transition: background 120ms, color 120ms;
+ transition: background var(--motion-fast) var(--ease-out-soft), color var(--motion-fast) var(--ease-out-soft);
}
.ob-present-modebtn:hover { background: var(--hover); color: hsl(var(--foreground)); }
@@ -1786,7 +1701,7 @@ a.obe-mention {
left: 50%;
transform: translateX(-50%);
opacity: 0;
- transition: opacity 160ms;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.ob-present-full:hover .ob-present-controls,
.ob-present-full .ob-present-controls:focus-within { opacity: 1; }
@@ -1797,7 +1712,7 @@ a.obe-mention {
align-items: center;
justify-content: center;
border: 1px solid hsl(var(--border));
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--background));
color: hsl(var(--foreground));
cursor: pointer;
@@ -1825,7 +1740,7 @@ a.obe-mention {
overflow: hidden;
aspect-ratio: 16 / 10;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-xl);
background: hsl(var(--background));
}
.ob-present-next-scale {
@@ -1843,30 +1758,26 @@ a.obe-mention {
.obe-codeblock {
flex: 1;
min-width: 0;
- background: hsl(var(--muted) / 0.6);
- border: 1px solid hsl(var(--border) / 0.7);
- border-radius: 8px;
+ background: var(--obe-code-bg);
+ border: 1px solid var(--obe-code-border);
+ border-radius: var(--radius-lg);
padding: 0.75rem 1rem;
margin: 0;
}
.obe-codeblock .obe-text {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 0.85rem;
- line-height: 1.55;
+ font-family: var(--obe-font-mono);
+ font-size: var(--obe-code-size);
+ line-height: var(--obe-code-leading);
}
/* Syntax-highlight tokens (blockeditor/highlight.ts). The tokenizer only wraps
runs in these spans — never alters the character stream — so highlighting is
purely cosmetic and the caret math is untouched. */
-.obe-tok-com { color: hsl(var(--muted-foreground)); font-style: italic; }
-.obe-tok-kw { color: hsl(265 60% 46%); }
-.obe-tok-str { color: hsl(140 46% 32%); }
-.obe-tok-num { color: hsl(28 78% 42%); }
-.obe-tok-lit { color: hsl(210 72% 45%); }
-.dark .obe-tok-kw { color: hsl(264 72% 78%); }
-.dark .obe-tok-str { color: hsl(140 48% 66%); }
-.dark .obe-tok-num { color: hsl(30 80% 66%); }
-.dark .obe-tok-lit { color: hsl(208 78% 70%); }
+.obe-tok-com { color: var(--obe-code-com); font-style: italic; }
+.obe-tok-kw { color: var(--obe-code-kw); }
+.obe-tok-str { color: var(--obe-code-str); }
+.obe-tok-num { color: var(--obe-code-num); }
+.obe-tok-lit { color: var(--obe-code-lit); }
/* Columns: a 12-col grid that collapses with its editor container. */
.obe-columns {
@@ -1915,9 +1826,9 @@ a.obe-mention {
align-items: center;
justify-content: center;
padding: 0.25rem;
- border-radius: 6px;
+ border-radius: var(--radius-md);
color: hsl(var(--muted-foreground));
- transition: color 120ms, background 120ms;
+ transition: color var(--motion-fast) var(--ease-out-soft), background var(--motion-fast) var(--ease-out-soft);
}
.obe-group-btn:hover:not(:disabled) { color: hsl(var(--foreground)); background: var(--hover); }
.obe-group-btn-on { color: hsl(var(--primary)); }
@@ -1945,7 +1856,7 @@ a.obe-mention {
.obe-table-fixed { table-layout: fixed; width: 100%; }
.obe-table-col-resizer {
position: absolute;
- z-index: calc(var(--z-index-local-overlay) - 1);
+ z-index: var(--z-index-pane-overlay);
top: 0;
bottom: 0;
width: 8px;
@@ -2010,10 +1921,10 @@ a.obe-mention {
display: grid;
place-items: center;
color: hsl(var(--muted-foreground) / 0.55);
- border-radius: 6px;
+ border-radius: var(--radius-md);
font-size: 13px;
opacity: 0;
- transition: opacity 120ms ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
cursor: pointer;
}
.obe-table-wrap:hover .obe-table-add { opacity: 1; }
@@ -2030,14 +1941,14 @@ a.obe-mention {
color: hsl(var(--muted-foreground));
opacity: 0;
pointer-events: none;
- transition: opacity 120ms ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.obe-table-wrap:hover .obe-table-tools,
.obe-table-tools:focus-within { opacity: 1; pointer-events: auto; }
.obe-table-tools button {
border: 1px solid hsl(var(--border));
background: hsl(var(--card));
- border-radius: 6px;
+ border-radius: var(--radius-md);
padding: 4px 8px;
cursor: pointer;
}
@@ -2067,7 +1978,7 @@ a.obe-mention {
color: hsl(var(--muted-foreground) / 0.7);
border-radius: var(--radius-sm);
opacity: 0;
- transition: opacity 120ms ease, background 120ms ease;
+ transition: opacity var(--motion-fast) var(--ease-out-soft), background var(--motion-fast) var(--ease-out-soft);
cursor: grab; /* explicit for WKWebView — desktop clicks need a pointer cursor */
z-index: var(--z-index-raised);
border: 0;
@@ -2132,13 +2043,12 @@ a.obe-mention {
overscroll-behavior: contain;
background: hsl(var(--popover));
border: 1px solid hsl(var(--border));
- border-radius: var(--radius-md);
+ border-radius: var(--radius-lg);
/* Quiet elevation: a hairline ring + a soft, tight shadow. */
box-shadow: var(--shadow-menu);
padding: 0.25rem;
- animation: obe-menu-enter var(--default-transition-duration) var(--ease-out-soft);
+ animation: obe-menu-enter var(--motion-base) var(--ease-out-soft);
}
-@media (prefers-reduced-motion: reduce) { .obe-slash { animation: none; } }
.obe-slash-item {
display: flex;
width: 100%;
@@ -2194,14 +2104,14 @@ a.obe-mention {
0% { background: hsl(var(--primary) / 0.3); }
100% { background: hsl(var(--accent)); }
}
-.ob-flow-pulse { animation: ob-flow-pulse 0.7s ease-out; }
+.ob-flow-pulse { animation: ob-flow-pulse 0.7s var(--ease-out-soft); }
/* Dataflow view: clicking a node walks you to its block in the editor. */
@keyframes obe-locate-flash {
0% { background: hsl(var(--primary) / 0.14); }
100% { background: transparent; }
}
-.obe-locate-flash { animation: obe-locate-flash 1.2s ease-out; border-radius: 8px; }
+.obe-locate-flash { animation: obe-locate-flash 1.2s var(--ease-out-soft); border-radius: var(--radius-lg); }
/* Database deep links (`?row=`/`?group=`): the targeted row/group header holds a
soft tint + ring briefly, then fades — a transient "here it is" after scrolling
@@ -2211,11 +2121,11 @@ a.obe-mention {
0%, 45% { background-color: hsl(var(--primary) / 0.16); box-shadow: 0 0 0 2px hsl(var(--ring) / 0.55); }
100% { background-color: transparent; box-shadow: 0 0 0 2px transparent; }
}
-.ob-anchor-flash { border-radius: 6px; }
+.ob-anchor-flash { border-radius: var(--radius-md); }
/* Motion-safe only: the JS scroll already falls back to `auto` under
reduced-motion, so keep the flash animation off there too. */
@media (prefers-reduced-motion: no-preference) {
- .ob-anchor-flash { animation: ob-anchor-flash 1.8s ease-out both; }
+ .ob-anchor-flash { animation: ob-anchor-flash 1.8s var(--ease-out-soft) both; }
}
/* Inline toolbar */
@@ -2288,7 +2198,7 @@ a.obe-mention {
cancelled by an equal negative margin, so the tint bleeds into the margins
while the text stays aligned with untinted blocks. */
.obe-blockbody[class*='obe-bg-'] {
- border-radius: 8px;
+ border-radius: var(--radius-lg);
padding: 0.25rem 0.5rem;
margin-inline: -0.5rem;
}
@@ -2296,65 +2206,38 @@ a.obe-mention {
.obe-blockbody[class*='obe-bg-'] > .obe-group { background: transparent; }
/* Text colours (block `fg`, inline `tc`). */
-.obe-fg-gray { color: hsl(0 0% 45%); }
-.obe-fg-brown { color: hsl(25 35% 38%); }
-.obe-fg-orange { color: hsl(28 80% 39%); }
-.obe-fg-yellow { color: hsl(42 80% 33%); }
-.obe-fg-green { color: hsl(140 50% 34%); }
-.obe-fg-blue { color: hsl(210 72% 45%); }
-.obe-fg-purple { color: hsl(265 55% 52%); }
-.obe-fg-pink { color: hsl(330 65% 48%); }
-.obe-fg-red { color: hsl(0 68% 48%); }
-.dark .obe-fg-gray { color: hsl(0 0% 66%); }
-.dark .obe-fg-brown { color: hsl(25 40% 62%); }
-.dark .obe-fg-orange { color: hsl(28 85% 62%); }
-.dark .obe-fg-yellow { color: hsl(45 80% 62%); }
-.dark .obe-fg-green { color: hsl(140 50% 60%); }
-.dark .obe-fg-blue { color: hsl(210 75% 67%); }
-.dark .obe-fg-purple { color: hsl(265 65% 73%); }
-.dark .obe-fg-pink { color: hsl(330 70% 71%); }
-.dark .obe-fg-red { color: hsl(0 72% 69%); }
+.obe-fg-gray { color: var(--obe-fg-gray); }
+.obe-fg-brown { color: var(--obe-fg-brown); }
+.obe-fg-orange { color: var(--obe-fg-orange); }
+.obe-fg-yellow { color: var(--obe-fg-yellow); }
+.obe-fg-green { color: var(--obe-fg-green); }
+.obe-fg-blue { color: var(--obe-fg-blue); }
+.obe-fg-purple { color: var(--obe-fg-purple); }
+.obe-fg-pink { color: var(--obe-fg-pink); }
+.obe-fg-red { color: var(--obe-fg-red); }
/* Block backgrounds (subtle tints). */
-.obe-bg-gray { background-color: hsl(0 0% 50% / 0.1); }
-.obe-bg-brown { background-color: hsl(25 45% 50% / 0.13); }
-.obe-bg-orange { background-color: hsl(28 85% 52% / 0.13); }
-.obe-bg-yellow { background-color: hsl(45 90% 52% / 0.15); }
-.obe-bg-green { background-color: hsl(140 55% 45% / 0.13); }
-.obe-bg-blue { background-color: hsl(210 80% 55% / 0.13); }
-.obe-bg-purple { background-color: hsl(265 60% 58% / 0.14); }
-.obe-bg-pink { background-color: hsl(330 70% 58% / 0.13); }
-.obe-bg-red { background-color: hsl(0 72% 55% / 0.12); }
-.dark .obe-bg-gray { background-color: hsl(0 0% 78% / 0.1); }
-.dark .obe-bg-brown { background-color: hsl(25 45% 55% / 0.18); }
-.dark .obe-bg-orange { background-color: hsl(28 80% 55% / 0.18); }
-.dark .obe-bg-yellow { background-color: hsl(45 80% 55% / 0.18); }
-.dark .obe-bg-green { background-color: hsl(140 50% 50% / 0.18); }
-.dark .obe-bg-blue { background-color: hsl(210 70% 55% / 0.2); }
-.dark .obe-bg-purple { background-color: hsl(265 55% 60% / 0.22); }
-.dark .obe-bg-pink { background-color: hsl(330 60% 58% / 0.2); }
-.dark .obe-bg-red { background-color: hsl(0 65% 55% / 0.2); }
+.obe-bg-gray { background-color: var(--obe-bg-gray); }
+.obe-bg-brown { background-color: var(--obe-bg-brown); }
+.obe-bg-orange { background-color: var(--obe-bg-orange); }
+.obe-bg-yellow { background-color: var(--obe-bg-yellow); }
+.obe-bg-green { background-color: var(--obe-bg-green); }
+.obe-bg-blue { background-color: var(--obe-bg-blue); }
+.obe-bg-purple { background-color: var(--obe-bg-purple); }
+.obe-bg-pink { background-color: var(--obe-bg-pink); }
+.obe-bg-red { background-color: var(--obe-bg-red); }
/* Inline highlights (stronger, with their own radius/padding). */
[class*='obe-hl-'] { border-radius: var(--radius-sm); padding: 0 4px; }
-.obe-hl-gray { background-color: hsl(0 0% 55% / 0.28); }
-.obe-hl-brown { background-color: hsl(25 50% 50% / 0.3); }
-.obe-hl-orange { background-color: hsl(30 90% 55% / 0.32); }
-.obe-hl-yellow { background-color: hsl(48 95% 55% / 0.4); }
-.obe-hl-green { background-color: hsl(140 60% 48% / 0.3); }
-.obe-hl-blue { background-color: hsl(210 85% 58% / 0.28); }
-.obe-hl-purple { background-color: hsl(265 65% 60% / 0.3); }
-.obe-hl-pink { background-color: hsl(330 75% 60% / 0.3); }
-.obe-hl-red { background-color: hsl(0 75% 58% / 0.28); }
-.dark .obe-hl-gray { background-color: hsl(0 0% 62% / 0.3); }
-.dark .obe-hl-brown { background-color: hsl(25 45% 50% / 0.34); }
-.dark .obe-hl-orange { background-color: hsl(30 80% 50% / 0.36); }
-.dark .obe-hl-yellow { background-color: hsl(48 80% 50% / 0.34); }
-.dark .obe-hl-green { background-color: hsl(140 50% 45% / 0.34); }
-.dark .obe-hl-blue { background-color: hsl(210 70% 52% / 0.36); }
-.dark .obe-hl-purple { background-color: hsl(265 55% 55% / 0.38); }
-.dark .obe-hl-pink { background-color: hsl(330 60% 55% / 0.36); }
-.dark .obe-hl-red { background-color: hsl(0 65% 52% / 0.36); }
+.obe-hl-gray { background-color: var(--obe-hl-gray); }
+.obe-hl-brown { background-color: var(--obe-hl-brown); }
+.obe-hl-orange { background-color: var(--obe-hl-orange); }
+.obe-hl-yellow { background-color: var(--obe-hl-yellow); }
+.obe-hl-green { background-color: var(--obe-hl-green); }
+.obe-hl-blue { background-color: var(--obe-hl-blue); }
+.obe-hl-purple { background-color: var(--obe-hl-purple); }
+.obe-hl-pink { background-color: var(--obe-hl-pink); }
+.obe-hl-red { background-color: var(--obe-hl-red); }
/* Swatch buttons (toolbar colour panel). */
.obe-sw {
@@ -2366,7 +2249,7 @@ a.obe-mention {
font-weight: 600;
font-size: 0.85rem;
cursor: pointer;
- border-radius: 6px;
+ border-radius: var(--radius-md);
}
.obe-sw:hover { outline: 2px solid hsl(var(--ring)); outline-offset: 1px; }
.obe-sw-fill { color: transparent; }
@@ -2382,7 +2265,7 @@ a.obe-mention {
margin-right: 0.5rem;
display: inline-grid;
place-items: center;
- border-radius: 4px;
+ border-radius: var(--radius-sm);
border: 1px solid hsl(var(--border));
font-size: 0.7rem;
font-weight: 700;
@@ -2397,7 +2280,7 @@ a.obe-mention {
display: block;
width: 0.9em;
height: 3px;
- border-radius: 999px;
+ border-radius: 9999px;
background: linear-gradient(90deg, hsl(0 70% 55%), hsl(45 90% 55%), hsl(210 80% 55%));
}
.obe-tb-colors {
@@ -2487,20 +2370,20 @@ a.obe-mention {
gap: 0.5rem;
background: hsl(var(--muted) / 0.5);
border: 1px solid hsl(var(--border) / 0.7);
- border-radius: 8px;
+ border-radius: var(--radius-lg);
padding: 0.5rem;
}
.obe-formula-src {
flex: 1;
min-width: 0;
background: transparent;
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
font-size: 0.85rem;
outline: none;
}
.obe-formula-eq { color: hsl(var(--muted-foreground) / 0.7); }
.obe-formula-out {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
font-size: 0.9rem;
font-weight: 600;
color: hsl(var(--primary));
@@ -2511,7 +2394,7 @@ a.obe-mention {
color: hsl(var(--muted-foreground));
font-size: 0.85rem;
border: 1px dashed hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
padding: 0.5rem;
}
@@ -2525,7 +2408,7 @@ a.obe-mention {
align-items: center;
gap: 0.75rem;
border: 1px dashed hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
padding: 0.5rem 0.75rem;
user-select: none;
}
@@ -2536,7 +2419,7 @@ a.obe-mention {
width: 2rem;
height: 2rem;
flex-shrink: 0;
- border-radius: 6px;
+ border-radius: var(--radius-md);
background: hsl(var(--accent) / 0.5);
font-size: 1.1rem;
color: hsl(var(--muted-foreground));
@@ -2553,7 +2436,7 @@ a.obe-mention {
.obe-missing-plugin-meta {
font-size: 0.7rem;
color: hsl(var(--muted-foreground) / 0.7);
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
margin-top: 0;
}
.obe-missing-plugin-error {
@@ -2569,12 +2452,12 @@ a.obe-mention {
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 500;
- border-radius: 6px;
+ border-radius: var(--radius-md);
border: 1px solid hsl(var(--border));
background: hsl(var(--background));
color: hsl(var(--foreground));
cursor: pointer;
- transition: background 0.15s, border-color 0.15s;
+ transition: background var(--motion-fast) var(--ease-out-soft), border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-missing-plugin-install:hover {
background: hsl(var(--accent));
@@ -2661,23 +2544,23 @@ a.obe-mention {
line-height: inherit;
color: inherit;
border: 1px solid transparent;
- border-radius: 4px;
+ border-radius: var(--radius-sm);
background: transparent;
padding: 0;
margin: 0;
cursor: text;
- transition: border-color 120ms, background 120ms;
+ transition: border-color var(--motion-fast) var(--ease-out-soft), background var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-inline:hover { border-color: hsl(var(--border) / 0.6); }
.obe-kit-inline:focus { outline: none; border-color: hsl(var(--ring)); background: hsl(var(--card)); }
-.obe-kit-inline::placeholder { color: hsl(var(--muted-foreground) / 0.55); }
+.obe-kit-inline::placeholder { color: var(--obe-placeholder); }
/* The inline description edits in place (WYSIWYG) and only renders when it has
text (see KitFrame) — so it never collapses/unfurls and never shifts the
layout. An empty description is added from the settings gear. */
.obe-kit-desc-edit {
opacity: 0.85;
- transition: border-color 120ms;
+ transition: border-color var(--motion-fast) var(--ease-out-soft);
}
/* The de-emphasized reactive symbol in a config card: a muted "Variable `x`"
@@ -2693,7 +2576,7 @@ a.obe-mention {
}
.obe-kit-symbol-hint:disabled { cursor: default; }
.obe-kit-symbol-code {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
color: hsl(var(--muted-foreground));
background: hsl(var(--muted) / 0.6);
border: 1px solid hsl(var(--border) / 0.6);
@@ -2703,7 +2586,7 @@ a.obe-mention {
.obe-kit-symbol-edit {
color: hsl(var(--primary));
opacity: 0;
- transition: opacity 120ms;
+ transition: opacity var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-symbol-hint:hover .obe-kit-symbol-edit { opacity: 1; }
.obe-kit-spacer { flex: 1; }
@@ -2726,7 +2609,7 @@ a.obe-mention {
cursor: pointer;
opacity: 0;
pointer-events: none;
- transition: opacity 120ms, color 120ms, background 120ms, border-color 120ms;
+ transition: opacity var(--motion-fast) var(--ease-out-soft), color var(--motion-fast) var(--ease-out-soft), background var(--motion-fast) var(--ease-out-soft), border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-kit:hover .obe-kit-gear,
.obe-kit:focus-within .obe-kit-gear,
@@ -2749,7 +2632,7 @@ a.obe-mention {
gap: 0.5rem 0.75rem;
border: 1px solid hsl(var(--border) / 0.7);
background: hsl(var(--muted) / 0.4);
- border-radius: 8px;
+ border-radius: var(--radius-lg);
padding: 0.5rem 0.75rem;
}
.obe-kit-field {
@@ -2763,15 +2646,15 @@ a.obe-mention {
.obe-kit-field-grow input { flex: 1; min-width: 0; }
.obe-kit-name, .obe-kit-num, .obe-kit-options, .obe-kit-select {
border: 1px solid hsl(var(--border));
- border-radius: 6px;
+ border-radius: var(--radius-md);
background: hsl(var(--card));
padding: 0.25rem 0.5rem;
font-size: 0.78rem;
color: hsl(var(--foreground));
}
-.obe-kit-name { width: 7rem; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
+.obe-kit-name { width: 7rem; font-family: var(--obe-font-mono); }
.obe-kit-num { width: 4rem; font-variant-numeric: tabular-nums; }
-.obe-kit-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
+.obe-kit-mono { font-family: var(--obe-font-mono); }
.obe-kit-empty { font-size: 0.78rem; color: hsl(var(--muted-foreground) / 0.7); font-style: italic; }
/* Number stepper */
@@ -2779,7 +2662,7 @@ a.obe-mention {
display: inline-flex;
align-items: stretch;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
overflow: hidden;
background: hsl(var(--card));
}
@@ -2790,7 +2673,7 @@ a.obe-mention {
font-size: 1rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
- transition: background 120ms, color 120ms;
+ transition: background var(--motion-fast) var(--ease-out-soft), color var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-stepper button:hover:not(:disabled) { background: var(--hover); color: hsl(var(--foreground)); }
.obe-kit-stepper input {
@@ -2809,7 +2692,7 @@ a.obe-mention {
flex: 1;
min-width: 8rem;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--card));
padding: 0.375rem 0.625rem;
font-size: 0.9rem;
@@ -2822,13 +2705,13 @@ a.obe-mention {
width: 1.8rem;
height: 1.8rem;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--card));
color: hsl(var(--muted-foreground));
font-size: 0.9rem;
cursor: pointer;
text-decoration: none;
- transition: color 120ms, border-color 120ms;
+ transition: color var(--motion-fast) var(--ease-out-soft), border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-mini:hover { color: hsl(var(--foreground)); border-color: hsl(var(--ring)); }
@@ -2836,13 +2719,13 @@ a.obe-mention {
.obe-kit-pills { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.obe-kit-pill {
border: 1px solid hsl(var(--border));
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--card));
padding: 0.25rem 0.75rem;
font-size: 0.82rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
- transition: color 120ms, background-color 120ms, border-color 120ms;
+ transition: color var(--motion-fast) var(--ease-out-soft), background-color var(--motion-fast) var(--ease-out-soft), border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-pill:hover:not(:disabled) { border-color: hsl(var(--ring)); color: hsl(var(--foreground)); }
.obe-kit-pill-on {
@@ -2898,11 +2781,11 @@ a.obe-mention {
}
.obe-form-kind {
border: 1px solid hsl(var(--border));
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--muted) / 0.55);
padding: 0 0.5rem;
color: hsl(var(--muted-foreground));
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
font-size: 0.68rem;
}
.obe-form-builder {
@@ -2916,7 +2799,7 @@ a.obe-mention {
}
.obe-form-empty {
border: 1px dashed hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
padding: 0.75rem;
color: hsl(var(--muted-foreground));
font-size: 0.82rem;
@@ -2933,7 +2816,7 @@ a.obe-mention {
width: 100%;
box-sizing: border-box;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--muted) / 0.28);
padding: 0.375rem 0.5rem;
color: hsl(var(--muted-foreground));
@@ -2969,7 +2852,7 @@ a.obe-mention {
}
.obe-form-state {
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--muted) / 0.35);
padding: 0.75rem;
color: hsl(var(--foreground));
@@ -3024,7 +2907,7 @@ a.obe-mention {
min-width: 0;
cursor: grab;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--muted) / 0.24);
padding: 0.375rem 0.5rem;
color: hsl(var(--foreground));
@@ -3053,7 +2936,7 @@ a.obe-mention {
align-items: center;
gap: 0.375rem;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--card));
padding: 0.375rem;
outline: none;
@@ -3079,7 +2962,7 @@ a.obe-mention {
height: 1.65rem;
cursor: pointer;
place-items: center;
- border-radius: 6px;
+ border-radius: var(--radius-md);
color: hsl(var(--muted-foreground));
}
.obe-form-field-grip { cursor: grab; touch-action: none; }
@@ -3096,7 +2979,7 @@ a.obe-mention {
}
.obe-form-row-copy span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.obe-form-required {
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--muted) / 0.7);
padding: 0 0.375rem;
color: hsl(var(--muted-foreground));
@@ -3136,7 +3019,7 @@ a.obe-mention {
cursor: pointer;
align-items: center;
gap: 0.375rem;
- border-radius: 4px;
+ border-radius: var(--radius-sm);
padding: 0.375rem 0.5rem;
font-size: var(--text-xs);
text-align: left;
@@ -3150,7 +3033,7 @@ a.obe-mention {
align-self: flex-start;
cursor: pointer;
border: 1px solid hsl(var(--border));
- border-radius: 6px;
+ border-radius: var(--radius-md);
padding: 0.25rem 0.5rem;
color: hsl(var(--foreground));
font-size: var(--text-xs);
@@ -3181,11 +3064,11 @@ a.obe-mention {
align-items: center;
gap: 0.5rem;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--card));
padding: 0.5rem 0.75rem;
font-size: 0.88rem;
- transition: border-color 120ms, background 120ms;
+ transition: border-color var(--motion-fast) var(--ease-out-soft), background var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-wide .obe-kit-pill:hover, .obe-kit-wide .obe-kit-check:hover {
background: hsl(var(--accent));
@@ -3198,7 +3081,7 @@ a.obe-mention {
.obe-kit-pill-dot {
width: 0.85rem;
height: 0.85rem;
- border-radius: 999px;
+ border-radius: 9999px;
border: 1.5px solid hsl(var(--muted-foreground) / 0.5);
flex-shrink: 0;
}
@@ -3217,12 +3100,12 @@ a.obe-mention {
.obe-kit-switch {
width: 2.4rem;
height: 1.35rem;
- border-radius: 999px;
+ border-radius: 9999px;
border: 1px solid hsl(var(--border));
background: hsl(var(--muted));
position: relative;
cursor: pointer;
- transition: background 150ms;
+ transition: background var(--motion-base) var(--ease-out-soft);
}
.obe-kit-switch-on { background: hsl(var(--primary)); border-color: hsl(var(--primary)); }
.obe-kit-knob {
@@ -3231,24 +3114,24 @@ a.obe-mention {
left: 2px;
width: 1.1rem;
height: 1.1rem;
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--background));
box-shadow: 0 1px 2px rgb(0 0 0 / 0.25);
- transition: transform 150ms;
+ transition: transform var(--motion-base) var(--ease-spring);
}
.obe-kit-switch-on .obe-kit-knob { transform: translateX(1rem); }
/* Action button */
.obe-kit-action {
border: 1px solid hsl(var(--primary));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
padding: 0.25rem 1rem;
font-size: 0.88rem;
font-weight: 550;
cursor: pointer;
- transition: opacity 120ms, background-color 120ms;
+ transition: opacity var(--motion-fast) var(--ease-out-soft), background-color var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-action:hover { opacity: 0.9; }
/* Colour-only press — deepen the fill toward --press-ink (matches Button), no geometry shift. */
@@ -3279,7 +3162,7 @@ a.obe-mention {
.obe-chart-xlabels text { text-anchor: middle; }
/* Sunburst centre readout (DASH-4): total at rest, hovered slice on hover. */
-.obe-chart-center-value { fill: hsl(var(--foreground)); font-size: 20px; font-weight: 650; font-variant-numeric: tabular-nums; }
+.obe-chart-center-value { fill: hsl(var(--foreground)); font-size: 20px; font-weight: var(--obe-heading-weight); font-variant-numeric: tabular-nums; }
.obe-chart-center-label { fill: hsl(var(--muted-foreground)); font-size: 11px; }
.obe-chart-legend text { text-anchor: start; font-size: 11px; }
.obe-chart-msg { fill: hsl(var(--muted-foreground) / 0.8); font-size: 12px; text-anchor: middle; }
@@ -3287,7 +3170,7 @@ a.obe-mention {
/* KPI / number tile (DASH-5) — the MetricCard look rendered in SVG. */
.obe-chart-kpi-caption { fill: hsl(var(--muted-foreground)); font-size: 12px; font-weight: 600; letter-spacing: 0.06em; text-anchor: middle; text-transform: uppercase; }
-.obe-chart-kpi-value { fill: hsl(var(--foreground)); font-size: 60px; font-weight: 650; text-anchor: middle; font-variant-numeric: tabular-nums; }
+.obe-chart-kpi-value { fill: hsl(var(--foreground)); font-size: 60px; font-weight: var(--obe-heading-weight); text-anchor: middle; font-variant-numeric: tabular-nums; }
.obe-chart-kpi-sub { fill: hsl(var(--muted-foreground)); font-size: 15px; text-anchor: middle; font-variant-numeric: tabular-nums; }
.obe-chart-kpi-track { fill: hsl(var(--muted)); }
@@ -3304,7 +3187,7 @@ a.obe-mention {
.obe-chart-plot { position: relative; }
/* Each datum's wrapper. Highlight is shared: the active mark brightens, the
rest de-emphasise, so every registered kind gets it from the scaffold. */
-.obe-chart-mark { cursor: pointer; outline: none; transition: opacity 0.12s ease, filter 0.12s ease; }
+.obe-chart-mark { cursor: pointer; outline: none; transition: opacity var(--motion-fast) var(--ease-out-soft), filter var(--motion-fast) var(--ease-out-soft); }
/* Forced-colors users get no cue from the token stroke ring, so restore a real
focus outline there (the `outline:none` above is fine for everyone else, who
see the is-active emphasis on focus). */
@@ -3318,7 +3201,7 @@ a.obe-mention {
.obe-chart-mark.is-active { filter: brightness(1.06); }
.obe-chart-mark.is-active :is(rect, path, circle) { stroke: hsl(var(--foreground) / 0.55); stroke-width: 1.5px; }
/* Line/scatter point markers read as part of the line at rest, surfacing on hover/focus. */
-.obe-chart-dot { fill-opacity: 0; transition: fill-opacity 0.12s ease; }
+.obe-chart-dot { fill-opacity: 0; transition: fill-opacity var(--motion-fast) var(--ease-out-soft); }
.obe-chart-mark.is-active .obe-chart-dot { fill-opacity: 1; }
.obe-chart-tooltip {
@@ -3351,7 +3234,7 @@ a.obe-mention {
.obe-kit-light {
width: 0.7rem;
height: 0.7rem;
- border-radius: 999px;
+ border-radius: 9999px;
flex-shrink: 0;
background: color-mix(in srgb, var(--data-gray, #c3c6cb) 35%, transparent);
box-shadow: inset 0 0 0 1px var(--data-stroke, rgba(0,0,0,0.12));
@@ -3361,7 +3244,7 @@ a.obe-mention {
.obe-kit-light-bad { background: var(--data-status-bad, #deaea6); box-shadow: inset 0 0 0 1px var(--data-stroke, rgba(0,0,0,0.12)), 0 0 0 3px color-mix(in srgb, var(--data-status-bad, #deaea6) 25%, transparent); }
.obe-kit-status-label { font-size: 0.88rem; font-weight: 550; }
.obe-kit-status-value {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
font-size: 0.82rem;
color: hsl(var(--muted-foreground));
}
@@ -3384,7 +3267,7 @@ a.obe-mention {
justify-content: center;
width: 0.95rem;
height: 0.95rem;
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
font-size: 0.65rem;
@@ -3400,7 +3283,7 @@ a.obe-mention {
background: hsl(var(--popover));
color: hsl(var(--popover-foreground));
border: 1px solid hsl(var(--border));
- border-radius: var(--radius-lg);
+ border-radius: var(--radius-md);
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
font-weight: 400;
@@ -3408,9 +3291,9 @@ a.obe-mention {
box-shadow: var(--shadow-menu);
opacity: 0;
pointer-events: none;
- transition: opacity 120ms;
+ transition: opacity var(--motion-fast) var(--ease-in-out-soft);
}
-.obe-kit-term:hover .obe-kit-tip, .obe-kit-term:focus .obe-kit-tip { opacity: 1; }
+.obe-kit-term:hover .obe-kit-tip, .obe-kit-term:focus .obe-kit-tip { opacity: 1; transition-timing-function: var(--ease-out-soft); }
/* Link card */
.obe-kit-linkcard-wrap { flex: 1; }
@@ -3425,7 +3308,7 @@ a.obe-mention {
padding: 0.5rem 0.75rem;
text-decoration: none;
color: inherit;
- transition: border-color 120ms, box-shadow 120ms;
+ transition: border-color var(--motion-fast) var(--ease-out-soft), box-shadow var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-linkcard:hover { border-color: hsl(var(--ring)); box-shadow: 0 2px 10px rgb(0 0 0 / 0.05); }
.obe-kit-linkcard-title { font-size: 0.92rem; font-weight: 600; }
@@ -3456,7 +3339,7 @@ a.obe-mention {
background: hsl(var(--card));
cursor: pointer;
text-align: left;
- transition: border-color 120ms, box-shadow 120ms;
+ transition: border-color var(--motion-fast) var(--ease-out-soft), box-shadow var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-card:hover:not(:disabled) { border-color: hsl(var(--ring)); }
.obe-kit-card-on { border-color: hsl(var(--primary)); box-shadow: 0 0 0 1px hsl(var(--primary)); }
@@ -3465,7 +3348,7 @@ a.obe-mention {
display: block;
width: 100%;
aspect-ratio: 16 / 10;
- border-radius: 6px;
+ border-radius: var(--radius-sm);
background-size: cover;
background-position: center;
background-color: hsl(var(--muted) / 0.6);
@@ -3487,7 +3370,7 @@ a.obe-mention {
justify-content: center;
width: 1.1rem;
height: 1.1rem;
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--primary));
color: hsl(var(--primary-foreground));
}
@@ -3498,7 +3381,7 @@ a.obe-mention {
min-height: 4.5rem;
resize: vertical;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--card));
padding: 0.5rem 0.75rem;
font: inherit;
@@ -3515,12 +3398,12 @@ a.obe-mention {
width: 1.6rem;
height: 1.6rem;
border: 1px solid hsl(var(--border));
- border-radius: 6px;
+ border-radius: var(--radius-md);
background: hsl(var(--card));
color: hsl(var(--muted-foreground));
font-size: 0.82rem;
cursor: pointer;
- transition: color 120ms, border-color 120ms;
+ transition: color var(--motion-fast) var(--ease-out-soft), border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-richtext-btn:hover { border-color: hsl(var(--ring)); color: hsl(var(--foreground)); }
.obe-kit-richtext-b { font-weight: 700; }
@@ -3529,7 +3412,7 @@ a.obe-mention {
.obe-kit-richtext-body {
min-height: 3rem;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-lg);
background: hsl(var(--card));
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
@@ -3545,7 +3428,7 @@ a.obe-mention {
/* Inline review: a quiet accent on blocks with an open suggestion or a comment,
plus link styling inside a rendered comment body. Presentation-only. */
.ob-block-reviewed {
- border-radius: 6px;
+ border-radius: var(--radius-md);
box-shadow: inset 2px 0 0 0 hsl(var(--primary) / 0.55);
background: hsl(var(--primary) / 0.05);
}
@@ -3574,7 +3457,7 @@ a.obe-mention {
.obe-rcursor-caret {
position: absolute;
width: 2px;
- border-radius: 999px;
+ border-radius: 9999px;
pointer-events: none;
}
/* A small nub at the caret top so a peer stays locatable after the label fades. */
@@ -3603,13 +3486,14 @@ a.obe-mention {
/* `color` is set inline per-swatch (readableTextColor) for WCAG AA contrast. */
pointer-events: none;
box-shadow: 0 1px 3px hsl(0 0% 0% / 0.18);
- animation: obe-rcursor-fade 2.8s ease forwards;
+ animation: obe-rcursor-fade 2.8s var(--ease-in-out-soft) forwards;
}
@keyframes obe-rcursor-fade {
0%, 65% { opacity: 1; }
100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
+ /* The forwards fill would override opacity after the global 0.01ms finish. */
.obe-rcursor-label { animation: none; opacity: 1; }
}
@@ -3622,17 +3506,17 @@ a.obe-mention {
min-width: 11rem;
max-width: 100%;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--card));
padding: 0.375rem 0.625rem;
font-size: 0.88rem;
color: hsl(var(--foreground));
cursor: pointer;
- transition: border-color 120ms;
+ transition: border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-kit-searchtrigger:hover:not(:disabled) { border-color: hsl(var(--ring)); }
.obe-kit-searchtrigger:disabled { opacity: 0.6; cursor: default; }
-.obe-kit-search-placeholder { color: hsl(var(--muted-foreground) / 0.7); }
+.obe-kit-search-placeholder { color: var(--obe-placeholder); }
/* Tag field: chips + a free-entry input. */
.obe-kit-tags {
@@ -3642,7 +3526,7 @@ a.obe-mention {
gap: 0.25rem;
width: 100%;
border: 1px solid hsl(var(--border));
- border-radius: 8px;
+ border-radius: var(--radius-md);
background: hsl(var(--card));
padding: 0.25rem 0.5rem;
min-height: 2rem;
@@ -3651,7 +3535,7 @@ a.obe-mention {
display: inline-flex;
align-items: center;
gap: 0.25rem;
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--muted) / 0.7);
padding: 0 0.25rem 0 0.5rem;
font-size: 0.8rem;
@@ -3660,7 +3544,7 @@ a.obe-mention {
display: inline-flex;
align-items: center;
justify-content: center;
- border-radius: 999px;
+ border-radius: 9999px;
color: hsl(var(--muted-foreground));
cursor: pointer;
}
@@ -3675,30 +3559,30 @@ a.obe-mention {
outline: none;
color: hsl(var(--foreground));
}
-.obe-kit-tag-input::placeholder { color: hsl(var(--muted-foreground) / 0.6); }
+.obe-kit-tag-input::placeholder { color: var(--obe-placeholder); }
/* Progress bar (display, no published value). */
.obe-kit-progress { flex-direction: column; align-items: stretch; gap: 0.25rem; }
.obe-kit-progress-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.5rem; }
.obe-kit-progress-label { font-size: 0.85rem; font-weight: 550; }
.obe-kit-progress-value {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
font-size: 0.8rem;
color: hsl(var(--muted-foreground));
}
.obe-kit-progress-track {
display: block;
height: 0.55rem;
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--muted) / 0.7);
overflow: hidden;
}
.obe-kit-progress-fill {
display: block;
height: 100%;
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--primary));
- transition: width 200ms ease;
+ transition: width var(--motion-base) var(--ease-out-soft);
}
/* ── Tabs & accordion containers ───────────────────────────────────────────── */
@@ -3724,12 +3608,12 @@ a.obe-mention {
display: inline-flex;
align-items: center;
gap: 0.25rem;
- border-radius: 6px;
+ border-radius: var(--radius-md);
padding: 0.25rem 0.5rem;
font-size: 0.78rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
- transition: color 120ms, background-color 120ms;
+ transition: color var(--motion-fast) var(--ease-out-soft), background-color var(--motion-fast) var(--ease-out-soft);
}
.obe-cnt-add:hover { color: hsl(var(--foreground)); background: var(--hover); }
@@ -3745,7 +3629,7 @@ a.obe-mention {
font-size: 0.84rem;
color: hsl(var(--muted-foreground));
cursor: pointer;
- transition: color 120ms, background-color 120ms, border-color 120ms;
+ transition: color var(--motion-fast) var(--ease-out-soft), background-color var(--motion-fast) var(--ease-out-soft), border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-tab:hover:not(:disabled) { background: var(--hover); color: hsl(var(--foreground)); }
.obe-tab-on {
@@ -3770,7 +3654,7 @@ a.obe-mention {
align-items: center;
justify-content: center;
color: hsl(var(--muted-foreground));
- border-radius: var(--radius-sm);
+ border-radius: var(--radius-md);
cursor: pointer;
}
.obe-acc-toggle:hover:not(:disabled) { color: hsl(var(--foreground)); background: var(--hover); }
@@ -3787,7 +3671,7 @@ a.obe-mention {
min-width: 1.1rem;
height: 1.1rem;
padding: 0 0.25rem;
- border-radius: 999px;
+ border-radius: 9999px;
background: hsl(var(--muted) / 0.8);
color: hsl(var(--muted-foreground));
font-size: 0.68rem;
@@ -3806,7 +3690,7 @@ a.obe-mention {
border-bottom: 1px solid hsl(var(--border) / 0.5);
}
.obe-code-lang-badge {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
font-size: 0.68rem;
letter-spacing: 0.02em;
text-transform: lowercase;
@@ -3826,7 +3710,7 @@ a.obe-mention {
padding: 0.25rem;
border-radius: var(--radius-md);
cursor: pointer;
- transition: color 120ms, background 120ms, border-color 120ms;
+ transition: color var(--motion-fast) var(--ease-out-soft), background var(--motion-fast) var(--ease-out-soft), border-color var(--motion-fast) var(--ease-out-soft);
}
.obe-code-btn:hover:not(:disabled) { background: var(--hover); color: hsl(var(--foreground)); }
.obe-code-btn:disabled { opacity: 0.45; cursor: default; }
@@ -3860,7 +3744,7 @@ a.obe-mention {
display: block;
padding: 0.25rem 0.75rem;
border-top: 1px solid hsl(var(--border) / 0.5);
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-family: var(--obe-font-mono);
font-size: 0.85rem;
color: hsl(var(--primary));
font-weight: 600;
@@ -3877,7 +3761,7 @@ a.obe-mention {
.obe-column { position: relative; }
.obe-col-divider {
position: absolute;
- z-index: 4;
+ z-index: var(--z-index-pane-overlay);
left: -1.125rem;
top: 0;
bottom: 0;
@@ -3895,7 +3779,7 @@ a.obe-mention {
width: 2px;
border-radius: var(--radius-sm);
background: transparent;
- transition: background 120ms ease;
+ transition: background var(--motion-fast) var(--ease-out-soft);
}
.obe-col-divider:hover::after,
.obe-col-divider:active::after,
@@ -3936,21 +3820,25 @@ a.obe-mention {
font-size: 12px;
font-weight: 600;
font-variant-numeric: tabular-nums;
- transition: border-color 0.12s ease;
+ transition: border-color var(--motion-fast) var(--ease-out-soft);
}
.ob-map-cluster-wrap:hover .ob-map-cluster { border-color: hsl(var(--ring) / 0.55); }
/* Meeting recorder: native playback, copyable transcript and ordinary note rows. */
-.obe-meeting { border: 1px solid var(--color-border); border-radius: var(--radius-md); padding: 16px; }
+.obe-meeting { border: 1px solid hsl(var(--border)); border-radius: var(--radius-lg); padding: 16px; }
.obe-meeting header, .obe-meeting-controls { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 12px; }
.obe-meeting header { justify-content: space-between; }
-.obe-meeting button { border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 6px 12px; cursor: pointer; }
+.obe-meeting button { border: 1px solid hsl(var(--border)); border-radius: var(--radius-md); padding: 6px 12px; cursor: pointer; background: hsl(var(--background)); color: hsl(var(--foreground)); transition: background-color var(--motion-fast) var(--ease-out-soft); }
+.obe-meeting button:hover:not(:disabled) { background: var(--hover); }
+.obe-meeting button:active { transition: none; }
+.obe-meeting-elapsed, .obe-meeting-lock { color: hsl(var(--muted-foreground)); font-size: var(--obe-small-size); }
+.obe-meeting-elapsed { font-variant-numeric: tabular-nums; }
.obe-meeting button:disabled { opacity: 0.5; cursor: default; }
-.obe-meeting button:focus-visible, .obe-meeting a:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
+.obe-meeting button:focus-visible, .obe-meeting a:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
.obe-meeting-chunks { display: grid; gap: 12px; margin-block: 12px; }
.obe-meeting-chunks li { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.obe-meeting audio { max-width: 100%; }
.obe-meeting-transcript { user-select: text; white-space: pre-wrap; }
-.obe-meeting time { color: var(--color-muted-foreground); font-variant-numeric: tabular-nums; }
+.obe-meeting time { color: hsl(var(--muted-foreground)); font-variant-numeric: tabular-nums; }
.obe-meeting-summary { user-select: text; white-space: pre-wrap; margin-block: 12px; }
-.obe-meeting-notes { border-top: 1px solid var(--color-border); margin-top: 16px; padding-top: 12px; }
+.obe-meeting-notes { border-top: 1px solid hsl(var(--border)); margin-top: 16px; padding-top: 12px; }
diff --git a/packages/ui/stylelint.config.mjs b/packages/ui/stylelint.config.mjs
index 068d6f33f..eb2759f83 100644
--- a/packages/ui/stylelint.config.mjs
+++ b/packages/ui/stylelint.config.mjs
@@ -1,3 +1,4 @@
+import designTokens from './scripts/design-token-stylelint.mjs';
const gridPx = String.raw`-?(?:4|6|8|10|12|16|20|24|28|32|36|40|44|48|56|64|80|96|112|128|144|160|176|192|208|224|240|256|288|320|384)px`;
const gridRem = String.raw`-?(?:0\.25|0\.375|0\.5|0\.625|0\.75|1|1\.25|1\.5|1\.75|2|2\.25|2\.5|2\.75|3|3\.5|4|5|6|7|8|9|10|11|12|13|14|15|16|18|20|24)rem`;
const variable = String.raw`var\([^)]*\)`;
@@ -8,18 +9,35 @@ const calculation = String.raw`calc\(\s*${calcInitialAtom}(?:\s+[-+*/]\s+${calcO
const percentage = String.raw`-?(?:[0-9]*\.)?[0-9]+%`;
const spacingAtom = String.raw`(?:0|auto|${gridPx}|${gridRem}|${percentage}|${variable}|${calculation})`;
const spacingValue = `/^${spacingAtom}(?:\\s+${spacingAtom}){0,3}$/`;
-const radiusAtom = String.raw`(?:0|50%|999px|9999px|4px|6px|8px|${variable}|${calculation}|calc\(var\(--radius\) - 2px\))`;
-const radiusValue = `/^${radiusAtom}(?:\\s+${radiusAtom}){0,3}$/`;
-
export default {
+ plugins: [designTokens],
// The package script intentionally passes only src/index.css; generated CSS
// and component-local styles are owned by other spacing clusters.
rules: {
+ // DSX warn-only baseline at foundation input: durations 44, easings ~40,
+ // literal colours 99, radius 84, editor font sizes ~120, z-index 1.
+ // Measured at 1d6ef346 with these guards: colour 101→36, radius 84→0,
+ // font-size 124→119, z-index 2→0 (includes a calculated local tier).
+ // Duration/easing combined findings 49→7: five attention timers and
+ // two global reduced-motion overrides remain intentionally literal.
+ 'design-tokens/role-values': true,
+ 'declaration-property-value-disallowed-list': [{
+ '/^(transition|animation)(-duration|-delay)?$/': [
+ '/(?