Theme Studio — AI-Powered Theme Generator
apps/theme-studio — Dependencies: @refrakt-md/transform, @refrakt-md/lumina, @refrakt-md/runes, @refrakt-md/svelte, @refrakt-md/ai
apps/theme-studio — Dependencies: @refrakt-md/transform, @refrakt-md/lumina, @refrakt-md/runes, @refrakt-md/svelte, @refrakt-md/ai
A standalone SvelteKit app that lets users create, customize, and export refrakt.md themes through a visual interface backed by AI generation. Users describe a theme in natural language ("dark cyberpunk with neon accents", "warm editorial magazine feel"), see a live preview of runes rendered with the generated tokens, tweak individual values through direct manipulation, and export a complete theme package.
The key insight: ~53 design tokens control the entire visual language. Changing tokens automatically updates all 48+ rune CSS files because rune CSS references tokens exclusively. This means AI-generated tokens produce an immediately usable theme without touching any rune CSS.
tokens/base.css + tokens/dark.css. A token-only theme layered on Lumina's rune CSS is a complete, functional theme.Generate tokens/base.css and tokens/dark.css. Import all of Lumina's rune CSS unchanged. This is the "change the visual language" approach — new colors, fonts, radii, shadows. Zero rune CSS to write.
Output:
tokens/base.css — ~53 CSS custom properties on :roottokens/dark.css — dark mode overrides via [data-theme="dark"] + @media (prefers-color-scheme: dark)manifest.json — theme metadataGenerate tokens plus override CSS for specific runes the user wants to customize. Layers after Lumina's full CSS import.
Output: Everything in Tier 1, plus:
styles/runes/{block}.css — per-rune CSS overridesindex.css barrel importGenerate tokens and all 48 rune CSS files from scratch. Requires deep structural knowledge derivable from baseConfig + contracts.
apps/theme-studio/ src/ app.html app.d.ts lib/ ai/ prompt.ts — System prompt for theme generation generate.ts — Token generation + refinement logic parse.ts — Extract CSS from AI responses theme/ tokens.ts — Token definitions, defaults, categories compiler.ts — Assemble tokens into CSS strings dark-mode.ts — Light↔dark token relationship mapping preview/ pipeline.ts — Markdoc parse → transform → render pipeline fixtures.ts — Sample rune content for preview showcase.svelte — Rune showcase grid state/ theme.svelte.ts — Reactive theme state (Svelte 5 runes) history.svelte.ts — Undo/redo stack components/ TokenEditor.svelte — Color/value editor for a single token TokenGroup.svelte — Grouped token editors (colors, typography, etc.) PromptBar.svelte — AI prompt input PreviewPanel.svelte — Live rune preview ExportPanel.svelte — Download/copy theme output ThemeHeader.svelte — Theme name, description, mode toggle routes/ +layout.svelte — App shell, theme injection +page.svelte — Main studio interface api/ generate/ +server.ts — AI generation endpoint package.json svelte.config.js vite.config.ts tsconfig.json
User prompt ("dark cyberpunk with neon accents") → API route → AI provider (Anthropic/Gemini/Ollama) → Structured token values (JSON) → Token state store (reactive) → CSS string assembly → Injected as <style> into preview iframe → Identity transform renders runes with BEM classes → Rune CSS (from Lumina) + generated tokens = styled preview
{ "name": "theme-studio", "private": true, "type": "module", "dependencies": { "@markdoc/markdoc": "0.4.0", "@refrakt-md/ai": "0.4.0", "@refrakt-md/lumina": "0.4.0", "@refrakt-md/runes": "0.4.0", "@refrakt-md/svelte": "0.4.0", "@refrakt-md/theme-base": "0.4.0", "@refrakt-md/transform": "0.4.0", "@refrakt-md/types": "0.4.0" }, "devDependencies": { "@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "svelte": "^5.49.2", "typescript": "^5.9.3", "vite": "^7.3.1" } }
The AI generates values for all ~53 tokens organized into these groups:
| Category | Tokens | Editor Type |
|---|---|---|
| Typography | font-sans, font-mono | Font picker (dropdown) |
| Primary Scale | color-primary-50 through color-primary-950 | Color scale generator (pick base, derive scale) |
| Core Palette | color-text, color-muted, color-border, color-bg, color-primary, color-primary-hover | Color pickers |
| Surfaces | color-surface, color-surface-hover, color-surface-active, color-surface-raised | Color pickers |
| Semantic | color-{info,warning,danger,success} + -bg + -border (12 total) | Color pickers (grouped by intent) |
| Radii | radius-sm, radius-md, radius-lg, radius-full | Pixel sliders |
| Shadows | shadow-xs, shadow-sm, shadow-md, shadow-lg | Shadow editor (visual) |
| Code | color-code-bg, color-code-text, color-inline-code-bg | Color pickers |
| Syntax | 7 Shiki tokens (--shiki-token-keyword, etc.) | Color pickers |
interface TokenDefinition { name: string; // CSS property name without prefix: "color-primary" cssVar: string; // Full CSS var: "--rf-color-primary" category: TokenCategory; type: 'color' | 'font' | 'size' | 'shadow'; default: string; // Lumina's default value description: string; // Human-readable purpose }
A static registry of all tokens with their metadata, defaults, and categories. This drives both the AI prompt (telling it what to generate) and the editor UI (what controls to render).
Dark mode tokens follow predictable patterns relative to light mode:
rgba(255, 255, 255, 0.1) pattern instead of named colorsrgba() for backgroundsThe AI generates both light and dark tokens together, but the UI can auto-derive reasonable dark mode defaults from light mode choices when the user manually edits individual tokens.
Theme generation requires a different system prompt than content authoring. The prompt teaches the AI about:
--rf-{category}-{name}), and what each one controls// Simplified prompt structure const THEME_SYSTEM_PROMPT = ` You are a theme designer for refrakt.md. Given a description, generate CSS design tokens that form a coherent visual theme. ## Token List ${tokenDefinitions.map(t => `- ${t.cssVar}: ${t.description} (type: ${t.type})`).join('\n')} ## Rules - Primary scale: 10 shades from lightest (50) to darkest (950), monotonically increasing in darkness - Semantic colors: info=blue family, warning=amber/orange, danger=red, success=green/teal (each needs main + bg + border) - Surfaces: bg → surface → surface-hover → surface-active must form a visible hierarchy - Contrast: text on bg must meet WCAG AA (4.5:1 minimum) - Radii: sm < md < lg, full=9999px - Shadows: progressive depth from xs to lg ## Output Format Return a JSON object with two keys: "light" and "dark". Each contains a flat map of token names (without --rf- prefix) to CSS values. `;
// POST /api/generate // Request: { prompt: string, provider?: string, model?: string, current?: TokenValues } // Response: SSE stream of JSON chunks // The endpoint: // 1. Builds system prompt with token vocabulary // 2. Includes current token state (if refining, not generating from scratch) // 3. Streams response from AI provider // 4. Client-side parser extracts JSON from response
After initial generation, the user can ask the AI to refine specific aspects:
The refinement prompt includes the current token state so the AI can make targeted adjustments rather than regenerating everything.
The AI returns a JSON object. The parser:
TokenValues objectinterface TokenValues { light: Record<string, string>; // token name → CSS value dark: Record<string, string>; }
The preview panel displays a user-configurable set of runes rendered through the full identity transform pipeline. This is not a mockup — it uses the actual @refrakt-md/transform engine with @refrakt-md/lumina's rune CSS, overlaid with the generated tokens.
Users select which runes appear in the preview column via a picker in the preview panel header. This keeps the preview focused on the runes that matter for their theme.
Picker UI: A toolbar button ("Runes") opens a popover checklist grouped by category. Each entry shows the fixture name and a brief label of what token groups it exercises. Users toggle individual fixtures on/off.
Presets: Quick-select buttons for common use cases:
| Preset | Fixtures Included |
|---|---|
| All | Every available fixture |
| Docs | Hints, Steps, Code, Tabs, Accordion, Prose |
| Marketing | Hero, Pricing, Feature Grid, CTA, Comparison |
| Blog | Prose, Timeline, Recipe, Blockquote, Code |
Token coverage indicator: When the selected fixtures don't exercise certain token categories, a subtle badge appears in the picker: "Not covered: semantic colors, code". This nudges users to add relevant fixtures without forcing a selection — they can ignore it if they're only tweaking specific tokens.
Persistence: The fixture selection is stored alongside the theme state in localStorage/IndexedDB and restored on reload.
Static Markdoc snippets covering the most visually distinct runes:
| Fixture | Runes Exercised | What It Tests | Token Groups |
|---|---|---|---|
| Hero section | Hero (center + left aligned) | Primary colors, typography, buttons, shadows | primary, typography, shadows |
| Hint variants | Hint (note, warning, caution, check) | All 4 semantic colors, borders, icons | semantic, borders |
| Pricing table | Pricing + Tier + FeaturedTier | Surfaces, borders, radii, featured accent | surfaces, borders, radii, primary |
| Tab group | TabGroup + Tab | Surface hierarchy, active states, borders | surfaces, borders |
| Code block | Fenced code | Code background, syntax highlighting tokens | code, syntax |
| Steps list | Steps + Step | Sequential layout, counters, primary accent | primary, typography |
| Feature grid | Grid + Feature | Card layout, surface colors, shadows | surfaces, shadows, radii |
| Timeline | Timeline + TimelineEntry | Line/dot accents, alternating layout | primary, borders |
| Recipe card | Recipe | Metadata display, badge styling | surfaces, typography |
| Data table | DataTable | Table styling, alternating rows, headers | surfaces, borders, typography |
| Accordion | Accordion + AccordionItem | Expand/collapse, borders, surface states | surfaces, borders |
| General prose | Headings, paragraphs, links, lists, blockquotes, inline code | Typography, text color, link color, muted color | typography, primary, code |
| CTA | CTA | Button styles, background contrast | primary, surfaces, typography |
| Comparison | Comparison | Column layout, highlighted state | surfaces, primary, borders |
| Blockquote | Blockquote | Border accent, muted text | borders, typography |
Default selection: All fixtures are enabled by default. The presets provide quick filtering for focused workflows.
Each fixture is a string of Markdoc content that gets parsed, transformed, serialized, and identity-transformed on the client. The Svelte Renderer displays the result inside an iframe that loads Lumina's rune CSS + the generated token stylesheet.
import Markdoc from '@markdoc/markdoc'; import { tags } from '@refrakt-md/runes'; import { serializeTree } from '@refrakt-md/svelte'; import { identityTransform } from '@refrakt-md/lumina/transform'; function renderFixture(markdoc: string): SerializedTag { const ast = Markdoc.parse(markdoc); const transformed = Markdoc.transform(ast, { tags }); const serialized = serializeTree(transformed); return identityTransform(serialized); }
The rendered tree is passed to Renderer.svelte from @refrakt-md/svelte inside the preview panel.
The preview renders inside an iframe to achieve complete CSS isolation:
index.css (rune CSS + default tokens)<style> tag is injected after Lumina's CSS, overriding :root tokens with generated values<style> — no re-render of the rune HTML neededdata-theme attribute on the iframe's <html> elementThis approach means:
┌──────────────────────────────────────────────────────────────┐ │ Theme Studio [Theme Name] [Light ◐ Dark] │ ├──────────────┬───────────────────────────────────────────────┤ │ │ [Runes ▾] [Docs] [Marketing] [Blog] [All] │ │ Token │ │ │ Editor │ Preview Panel │ │ │ │ │ ┌────────┐ │ ┌─────────────────────────────────────────┐ │ │ │ Colors │ │ │ Hero Section │ │ │ │ ●●●●● │ │ │ ═══════════ │ │ │ │ │ │ │ Hints (note, warning, caution, check) │ │ │ │ Typo │ │ │ Pricing Table │ │ │ │ Aa Bb │ │ │ Code Block │ │ │ │ │ │ │ Steps │ │ │ │ Radii │ │ │ ... │ │ │ │ ▢ ▢ ▢ │ │ │ │ │ │ │ │ │ │ ⚠ Not covered: code, syntax │ │ │ │ Shadow │ │ └─────────────────────────────────────────┘ │ │ │ ░░░░░ │ │ │ │ └────────┘ │ │ │ │ │ ├──────────────┴───────────────────────────────────────────────┤ │ [✦ Describe your theme...] [Generate] │ └──────────────────────────────────────────────────────────────┘
Prompt Bar (bottom): Text input for AI generation/refinement. "Dark cyberpunk with neon accents" or "Make the shadows more dramatic". Supports initial generation and iterative refinement.
Token Editor (left sidebar): Grouped editors for all token categories. Each group is collapsible. Individual tokens show:
Preview Panel (main area): Scrollable showcase of rune fixtures rendered with current tokens. Light/dark mode toggle in the header. Responsive — shows how runes look at different widths. A toolbar row at the top provides:
| Action | Result |
|---|---|
| Type prompt + Generate | AI generates all tokens, preview updates, editor populates |
| Edit single token | Preview updates instantly (CSS variable change) |
| Toggle light/dark | Preview switches mode, editor shows corresponding token set |
| Type refinement prompt | AI adjusts specific tokens, preserving manual edits where possible |
| Toggle fixture in Runes picker | Preview adds/removes that rune, coverage badge updates |
| Click preset (Docs/Marketing/Blog/All) | Fixture selection switches to preset, preview re-renders |
| Undo/Redo | Token state reverts/reapplies (full snapshot) |
| Export | Download theme package as zip or copy CSS to clipboard |
| Key | Action |
|---|---|
Ctrl+Z / Cmd+Z | Undo |
Ctrl+Shift+Z / Cmd+Shift+Z | Redo |
Ctrl+Enter / Cmd+Enter | Submit prompt |
Ctrl+E / Cmd+E | Toggle export panel |
Ctrl+D / Cmd+D | Toggle light/dark preview |
// lib/state/theme.svelte.ts interface ThemeState { name: string; description: string; tokens: { light: Record<string, string>; // token name → value dark: Record<string, string>; }; overrides: { light: Set<string>; // tokens manually edited by user dark: Set<string>; }; mode: 'light' | 'dark'; selectedFixtures: Set<string>; // fixture IDs enabled in preview }
The overrides sets track which tokens the user has manually edited. When the AI regenerates, it preserves overridden tokens unless explicitly told to reset them. This enables iterative refinement: generate a base, hand-tune a few values, ask AI to adjust the rest.
// lib/state/history.svelte.ts interface HistoryEntry { tokens: ThemeState['tokens']; overrides: ThemeState['overrides']; label: string; // "AI generation", "Edit --rf-color-primary", etc. }
Captures full token snapshots on each meaningful change (AI generation, manual edit, batch operation). Provides undo/redo with human-readable labels.
The export produces a downloadable zip or copyable file set:
my-theme/ package.json — Package metadata manifest.json — Theme config (name, prefix, darkMode) src/ config.ts — mergeThemeConfig(baseConfig, { icons: {...} }) transform.ts — createTransform(config) re-export tokens/ base.css — Generated light mode tokens dark.css — Generated dark mode tokens svelte/ index.ts — Re-export from @refrakt-md/theme-base index.css — Imports Lumina base + token overrides
index.css StrategyThe generated index.css imports Lumina's full CSS as a base, then the custom tokens override via cascade:
/* Import Lumina's complete rune CSS + default tokens */ @import '@refrakt-md/lumina/index.css'; /* Override with custom tokens */ @import './tokens/base.css'; @import './tokens/dark.css';
Because Lumina's rune CSS references tokens via var(--rf-*), the custom tokens take precedence and restyle everything.
config.tsimport { baseConfig, mergeThemeConfig } from '@refrakt-md/theme-base'; export const myThemeConfig = mergeThemeConfig(baseConfig, { // Icons inherited from base (Lumina provides SVGs) });
For Tier 1 (token-only), the config is identical to Lumina's — no rune config overrides needed.
For quick use without a full package, users can copy:
base.css contentdark.css contentindex.css with importsThe current theme system resolves themes via Node module resolution — the theme field in refrakt.config.json is used as a bare specifier in import(), import.meta.resolve(), and generated virtual module imports (plugin.ts:35,66,72, virtual-modules.ts:55,61-63). This means a theme must be resolvable by Node, but it does not have to come from the npm registry.
Theme Studio exports a tarball (.tgz). A CLI command installs it into the project:
npx refrakt theme install ./my-theme-1.0.0.tgz
Under the hood:
npm, pnpm, yarn, bun) via lockfile heuristicnpm install file:./my-theme-1.0.0.tgz)name field from the tarball's package.jsonrefrakt.config.json to set "theme": "<package-name>"The theme lands in node_modules/ and is fully resolvable by all existing import paths. No changes to the SvelteKit plugin required.
User experience:
1. Build theme in Theme Studio 2. Download my-theme-1.0.0.tgz 3. npx refrakt theme install ./my-theme-1.0.0.tgz 4. npm run dev — site is restyled
Tarball contents (produced by npm pack on the exported theme directory):
package/ package.json — name, version, exports map manifest.json — theme metadata dist/ config.js — compiled mergeThemeConfig() call config.d.ts transform.js — createTransform(config) re-export transform.d.ts tokens/ base.css — generated light mode tokens dark.css — generated dark mode tokens svelte/ index.ts — SvelteTheme re-export tokens.css — CSS bridge (imports index.css) styles/ runes/ — empty for Tier 1, populated for Tier 2+ index.css — barrel import (Lumina base + token overrides) base.css — tokens + global + layouts (no runes)
The package.json inside the tarball includes the same exports map pattern as Lumina, ensuring all subpath imports work:
{ "name": "my-theme", "version": "1.0.0", "type": "module", "exports": { ".": "./index.css", "./base.css": "./base.css", "./transform": { "default": "./dist/transform.js" }, "./svelte": { "svelte": "./svelte/index.ts", "default": "./svelte/index.ts" }, "./styles/runes/*.css": "./styles/runes/*.css", "./svelte/tokens.css": "./svelte/tokens.css" }, "dependencies": { "@refrakt-md/theme-base": "^0.4.0", "@refrakt-md/transform": "^0.4.0", "@refrakt-md/lumina": "^0.4.0" }, "peerDependencies": { "svelte": "^5.0.0" }, "peerDependenciesMeta": { "svelte": { "optional": true } } }
Note: @refrakt-md/lumina is listed as a dependency because Tier 1 themes import Lumina's rune CSS via @import '@refrakt-md/lumina/index.css' in their index.css. The user's project already has Lumina (or it gets installed transitively).
Updating a theme: Re-run npx refrakt theme install ./my-theme-1.1.0.tgz. The CLI detects the existing theme, bumps the version, and reinstalls. No manual package.json edits needed.
For a zero-install experience, extend the SvelteKit plugin to resolve local directory paths as themes:
// refrakt.config.json
{
"theme": "./themes/my-theme",
"target": "svelte",
"contentDir": "./content"
}
The user unzips the exported theme into themes/my-theme/ and points the config at it. No npm install step.
Plugin changes required:
The plugin detects relative paths (starts with ./ or ../) and adjusts resolution:
plugin.ts config hook — resolve the relative path to an absolute path. Add Vite resolve.alias entries so that bare specifier sub-path imports work:// When theme starts with './' or '../': const absTheme = resolve(resolvedRoot, refraktConfig.theme); config.resolve.alias = { [themeName]: absTheme, // 'my-theme' → '/abs/themes/my-theme' [`${themeName}/transform`]: `${absTheme}/dist/transform.js`, [`${themeName}/svelte`]: `${absTheme}/svelte/index.ts`, [`${themeName}/base.css`]: `${absTheme}/base.css`, [`${themeName}/svelte/tokens.css`]: `${absTheme}/svelte/tokens.css`, };
plugin.ts buildStart — use resolve() instead of import.meta.resolve() to locate the theme root for CSS tree-shaking.
virtual-modules.ts — no changes needed if aliases are set up correctly; the generated import statements still use the theme name as a specifier, and Vite resolves them through aliases.
SSR noExternal — the local path must also be added, or marked as external: false, so Vite bundles it during SSR.
Exported directory structure is identical to the tarball contents (minus the package/ wrapper). Theme Studio provides a "Download as ZIP" option that extracts directly into the project.
User experience:
1. Build theme in Theme Studio 2. Download my-theme.zip 3. Unzip into ./themes/my-theme/ 4. Set "theme": "./themes/my-theme" in refrakt.config.json 5. npm run dev — site is restyled
| CLI Install (tarball) | Local Directory | |
|---|---|---|
| Plugin changes | None | Alias resolution + path detection |
| npm install needed | Yes (automated by CLI) | No |
| Version management | npm handles it | Manual (overwrite directory) |
| Lockfile tracking | Yes — pinned version | No |
| CI reproducibility | Excellent — lockfile pins exact version | Depends on committing the theme dir |
| Peer deps | Resolved automatically by npm | Must be installed separately |
| Multiple themes | Switch by changing config + installing | Switch by changing config path |
The tarball/CLI path is the safe default — it works within the existing npm ecosystem, gives version tracking, and requires zero plugin changes. The local directory path is a convenience feature that removes friction for rapid iteration ("download, unzip, done") at the cost of plugin complexity.
refrakt theme install <path> Install a theme from a .tgz tarball refrakt theme install <name> Install a theme from the npm registry refrakt theme list List available themes (registry + local) refrakt theme info Show current theme details
refrakt theme install implementation:
async function themeInstall(source: string): Promise<void> { // 1. Detect package manager const pm = detectPackageManager(); // npm | pnpm | yarn | bun // 2. Read theme name from tarball or registry const themeName = source.endsWith('.tgz') ? readNameFromTarball(source) : source; // 3. Install const installCmd = { npm: `npm install ${source}`, pnpm: `pnpm add ${source}`, yarn: `yarn add ${source}`, bun: `bun add ${source}`, }[pm]; await exec(installCmd); // 4. Update refrakt.config.json const config = loadRefraktConfig('./refrakt.config.json'); config.theme = themeName; writeFileSync('./refrakt.config.json', JSON.stringify(config, null, '\t') + '\n'); console.log(`Theme "${themeName}" installed and set as active theme.`); }
Theme state persists to localStorage (or IndexedDB via the idb package, matching the chat app pattern):
Theme tokens can be encoded into a shareable URL parameter (compressed JSON). This enables:
apps/theme-studio/ with package.json, configsbase.css and dark.css strings@refrakt-md/ai provider infrastructurenpm pack equivalent in-browser (tar.gz generation)refrakt theme install CLI — Detect package manager, install tarball, update config./ theme paths via Vite aliasesapps/theme-studio/lib/tokens.ts)lib/state/theme.svelte.ts)base.css / dark.css strings (lib/compiler.ts)lib/pipeline.ts)lib/ai/prompt.ts)routes/api/generate/+server.ts)lib/ai/parse.ts)lib/state/generate.svelte.ts)routes/+page.svelte)lib/editors/)lib/TokenGroup.svelte)lib/PreviewPanel.svelte)lib/PromptBar.svelte)lib/FixturePicker.svelte)@refrakt-md/behaviorslib/PromptBar.svelte)lib/ExportPanel.svelte)lib/export.ts)lib/state/history.svelte.ts)lib/state/persistence.svelte.ts)CSS isolation. The studio's own UI uses its own styles. The preview must render with Lumina's rune CSS + generated tokens without interference. An iframe provides a clean document context — identical to how the theme will work in production.
Lumina's rune CSS is already written, tested, and references tokens exclusively. Generating 48 CSS files adds enormous complexity and error surface for zero visual benefit — the tokens already control every visual property. Tier 1 gets 95% of the way there with ~53 token values.
Token generation involves substantial AI reasoning (color theory, accessibility math, design coherence). Streaming shows progress and lets the UI update progressively — e.g., populating color swatches as they arrive rather than waiting for the full response.
The AI provider calls require server-side API keys. SvelteKit's API routes handle this cleanly. The app could be deployed as a static SPA with client-side AI calls in the future, but the server route pattern matches the existing chat app and keeps keys off the client.
Theme generation is a fundamentally different interaction model. The chat app is a conversation with document curation. Theme Studio is a visual editor with AI assistance. Separate apps avoid UI compromises and keep each focused on its purpose.
generateStructureContract() (scripts/generate-contracts.ts)lib/RuneEditor.svelte)lib/ai/css-prompt.ts, state/generate-css.svelte.ts)<style> tag (PreviewPanel.svelte)styles/runes/{block}.css files + updated index.css importsNow that the icon rune system exists ({% icon /%}, Lucide registry, grouped icon config), this tier expands beyond hint-only icons:
packages/lumina/src/icons.tsmask-image data URIs in hint rune overridesicons.global in mergeThemeConfig{% icon /%} for global groupcurrentColor-compatible)mask-image overrides for structure-injected icons AND icons config object for mergeThemeConfig.tgz generation for npm-compatible packagesrefrakt theme install CLI command — detect package manager, install, update config./ theme paths