anentrypoint-design 0.0.385 → 0.0.387

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/247420.css +33 -0
  2. package/dist/247420.js +53 -51
  3. package/package.json +1 -1
  4. package/src/components/agent-chat/controls.js +143 -0
  5. package/src/components/agent-chat/empty-state.js +63 -0
  6. package/src/components/agent-chat/message-rows.js +141 -0
  7. package/src/components/agent-chat/surface.js +206 -0
  8. package/src/components/agent-chat/thread-behaviour.js +50 -0
  9. package/src/components/agent-chat.js +6 -546
  10. package/src/components/chat/composer-affordances.js +109 -0
  11. package/src/components/chat/composer.js +256 -0
  12. package/src/components/chat/threads.js +105 -0
  13. package/src/components/chat-message-parts/agent-nodes.js +103 -0
  14. package/src/components/chat-message-parts/inline.js +106 -0
  15. package/src/components/chat-message-parts/prose-nodes.js +133 -0
  16. package/src/components/chat-message-parts/renderers.js +89 -0
  17. package/src/components/chat-message-parts.js +12 -408
  18. package/src/components/chat-minimap/minimap.js +167 -0
  19. package/src/components/chat-minimap/paint.js +73 -0
  20. package/src/components/chat-minimap/preview.js +41 -0
  21. package/src/components/chat-minimap.js +7 -263
  22. package/src/components/chat.js +20 -628
  23. package/src/components/community/chrome.js +63 -0
  24. package/src/components/community/navigation.js +167 -0
  25. package/src/components/community/presence.js +106 -0
  26. package/src/components/community/shell.js +17 -0
  27. package/src/components/community/views.js +131 -0
  28. package/src/components/community.js +18 -447
  29. package/src/components/files/chrome.js +140 -0
  30. package/src/components/files/entries.js +156 -0
  31. package/src/components/files/grid-controls.js +63 -0
  32. package/src/components/files/grid.js +177 -0
  33. package/src/components/files/types.js +69 -0
  34. package/src/components/files-modals/dialogs.js +118 -0
  35. package/src/components/files-modals/modal-shell.js +153 -0
  36. package/src/components/files-modals/preview-bodies.js +129 -0
  37. package/src/components/files-modals/preview-containers.js +96 -0
  38. package/src/components/files-modals.js +14 -475
  39. package/src/components/files.js +15 -564
  40. package/src/components/freddie/pages-config.js +3 -94
  41. package/src/components/freddie/pages-models.js +97 -0
  42. package/src/components/freddie.js +2 -1
  43. package/src/components/interaction-primitives/mobile.js +25 -0
  44. package/src/components/interaction-primitives/pointer.js +214 -0
  45. package/src/components/interaction-primitives/reorderable.js +47 -0
  46. package/src/components/interaction-primitives/shortcuts.js +119 -0
  47. package/src/components/interaction-primitives.js +16 -388
  48. package/src/components/sessions/conversation-list.js +230 -0
  49. package/src/components/sessions/dashboard.js +202 -0
  50. package/src/components/sessions/detail-bits.js +49 -0
  51. package/src/components/sessions/format.js +42 -0
  52. package/src/components/sessions/session-card.js +92 -0
  53. package/src/components/sessions.js +16 -579
  54. package/src/components/voice/audio-cue.js +39 -0
  55. package/src/components/voice/capture.js +90 -0
  56. package/src/components/voice/playback.js +75 -0
  57. package/src/components/voice/settings-modal.js +104 -0
  58. package/src/components/voice.js +16 -293
  59. package/src/css/app-shell/base.css +23 -0
  60. package/src/css/app-shell/states-interactions.css +10 -0
  61. package/src/kits/os/freddie/chat-protocol.js +32 -0
  62. package/src/kits/os/freddie/chat-transport.js +178 -0
  63. package/src/kits/os/freddie/pages-chat.js +10 -180
  64. package/src/kits/os/shell-chrome.js +163 -0
  65. package/src/kits/os/shell-geometry.js +59 -0
  66. package/src/kits/os/shell.js +178 -335
  67. package/src/page-html/client-script.js +151 -0
  68. package/src/page-html/head-tags.js +59 -0
  69. package/src/page-html/markdown.js +68 -0
  70. package/src/page-html/page-styles.js +44 -0
  71. package/src/page-html.js +14 -291
@@ -0,0 +1,151 @@
1
+ // The client-side mount script the SSR document embeds, as a string. It is
2
+ // NOT module code in this file's own scope: it ships verbatim inside the
3
+ // emitted <script type="module"> and runs in the browser against the SDK's
4
+ // `mount`/`components`/`h` exports plus the `__site__` JSON payload.
5
+ //
6
+ // It stays a template literal (not a real module compiled and inlined) because
7
+ // it must reach the browser as source with its own escaping intact — every
8
+ // backslash/backtick sequence below is written for the emitted output, not for
9
+ // this file. `clientScriptExtra` is appended raw after the mount() call.
10
+
11
+ export const CLIENT_SCRIPT = `import { mount, components as C, h } from 'anentrypoint-design';
12
+ const data = JSON.parse(document.getElementById('__site__').textContent);
13
+
14
+ function heroNode(hero) {
15
+ if (!hero) return null;
16
+ const badges = Array.isArray(hero.badges) ? hero.badges.filter(Boolean) : [];
17
+ const badgeRow = badges.length
18
+ ? h('div', { class: 'ds-hero-stats' }, ...badges.map((b, i) =>
19
+ h('span', { key: i, class: 'ds-hero-stat' },
20
+ h('strong', { class: 'ds-hero-stat-n' }, String(b.label != null ? b.label : b)),
21
+ b.desc ? h('span', { class: 'ds-hero-stat-l' }, String(b.desc)) : null,
22
+ )))
23
+ : null;
24
+ return h('div', { class: 'ds-hero' },
25
+ hero.eyebrow ? h('span', { class: 'eyebrow' }, hero.eyebrow) : null,
26
+ h('h1', { class: 'ds-hero-title' }, hero.heading || hero.title || data.title),
27
+ (hero.body || hero.subheading) ? h('p', { class: 'ds-hero-body' },
28
+ hero.body || hero.subheading,
29
+ hero.accent ? h('span', { class: 'ds-hero-accent' }, ' ' + hero.accent) : null,
30
+ ) : null,
31
+ Array.isArray(hero.ctas) && hero.ctas.length
32
+ ? h('div', { class: 'ds-hero-actions' }, ...hero.ctas.map((c, i) =>
33
+ h('a', { key: i, class: i === 0 ? 'btn btn-accent' : 'btn btn-ghost', href: c.href || '#' }, c.label || c.cta || 'go')))
34
+ : null,
35
+ badgeRow,
36
+ );
37
+ }
38
+
39
+ function sectionNode(sec, idx) {
40
+ const features = sec.features || sec.items || [];
41
+ const rows = features.map((f, i) => {
42
+ const kids = [h('div', { key: 't', class: 'ds-feature-title' }, String(f.name || ''))];
43
+ if (f.desc) kids.push(h('div', { key: 'd', class: 'ds-feature-desc', innerHTML: String(f.desc).replace(/\`([^\`]+)\`/g, '<code>$1</code>') }));
44
+ if (f.benefit) kids.push(h('div', { key: 'b', class: 'ds-feature-benefit' }, String(f.benefit)));
45
+ return h('div', { key: i, class: 'ds-feature' }, ...kids);
46
+ });
47
+ return C.Section({
48
+ id: sec.id || null,
49
+ title: sec.name || sec.title || sec.id,
50
+ children: [
51
+ sec.lede ? h('p', { class: 'ds-lede' }, sec.lede) : null,
52
+ ...rows,
53
+ sec.body && String(sec.body).trim() ? h('div', { class: 'page-body', innerHTML: __md(sec.body) }) : null,
54
+ ].filter(Boolean),
55
+ });
56
+ }
57
+
58
+ function examplesNode(examples) {
59
+ if (!examples || !examples.length) return null;
60
+ return C.Section({
61
+ title: 'explore',
62
+ children: examples.map((e, i) => {
63
+ const kids = [
64
+ h('span', { key: 'c', class: 'code' }, String(i + 1).padStart(2, '0')),
65
+ h('span', { key: 't', class: 'title' }, String(e.label || e.name || e.href || '')),
66
+ ];
67
+ if (e.desc) kids.push(h('span', { key: 'm', class: 'meta dim' }, ' — ' + e.desc));
68
+ kids.push(h('span', { key: 'a', class: 'ds-row-arrow' }, '->'));
69
+ return h('a', { key: i, class: 'row', href: e.href || '#' }, ...kids);
70
+ }),
71
+ });
72
+ }
73
+
74
+ function panelNode(panel, idx) {
75
+ const items = Array.isArray(panel.items) ? panel.items : [];
76
+ if (!items.length) return null;
77
+ const rows = items.map((it, i) => {
78
+ const kids = [
79
+ h('span', { key: 'c', class: 'code' }, String(it.code || String(i + 1).padStart(2, '0'))),
80
+ h('span', { key: 't', class: 'title' }, String(it.title || it.name || '')),
81
+ ];
82
+ if (it.sub || it.desc) kids.push(h('span', { key: 'm', class: 'meta dim' }, ' — ' + (it.sub || it.desc)));
83
+ kids.push(h('span', { key: 'a', class: 'ds-row-arrow' }, it.meta || '->'));
84
+ return h('a', { key: i, class: 'row', href: it.href || '#' }, ...kids);
85
+ });
86
+ return C.Panel({ id: panel.id || null, title: panel.title || panel.name || '', count: panel.count || items.length, children: rows });
87
+ }
88
+
89
+ function marqueeNode(marquee) {
90
+ if (!marquee || !Array.isArray(marquee.items) || !marquee.items.length) return null;
91
+ return C.Marquee ? C.Marquee({ items: marquee.items, sep: marquee.sep || '/' }) : null;
92
+ }
93
+
94
+ function quickstartNode(quickstart) {
95
+ if (!quickstart || !Array.isArray(quickstart.lines) || !quickstart.lines.length) return null;
96
+ const lineNodes = quickstart.lines.map((l, i) => h('div', { key: 'q' + i, class: 'cli' },
97
+ h('span', { class: 'prompt' }, l.kind === 'cmt' ? '#' : '$'),
98
+ h('span', { class: 'cmd' }, l.text)
99
+ ));
100
+ return C.Panel({ title: quickstart.heading || 'quick start', children: h('div', { class: 'ds-quickstart' }, ...lineNodes) });
101
+ }
102
+
103
+ function sideNode(sidebar) {
104
+ if (!sidebar || !Array.isArray(sidebar.sections) || !sidebar.sections.length || !C.Side) return null;
105
+ return C.Side({ sections: sidebar.sections });
106
+ }
107
+
108
+ // minimal client-side markdown renderer matching server-side renderer (idempotent for already-html bodies)
109
+ function __slug(s) { return String(s || '').trim().toLowerCase().replace(/[^\\w\\s-]/g, '').replace(/\\s+/g, '-'); }
110
+ function __md(md) {
111
+ const lines = String(md || '').split('\\n');
112
+ const out = []; let inCode = false, inList = false;
113
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
114
+ const inl = (s) => s.replace(/\`([^\`]+)\`/g, '<code>$1</code>').replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>').replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>');
115
+ for (const line of lines) {
116
+ if (line.startsWith('\`\`\`')) { if (inCode) { out.push('</pre>'); inCode = false; } else { out.push('<pre>'); inCode = true; } continue; }
117
+ if (inCode) { out.push(esc(line)); continue; }
118
+ if (line.startsWith('# ')) { const t = line.slice(2); out.push('<h1 id="' + __slug(t) + '">' + esc(t) + '</h1>'); }
119
+ else if (line.startsWith('## ')) { const t = line.slice(3); out.push('<h2 id="' + __slug(t) + '">' + esc(t) + '</h2>'); }
120
+ else if (line.startsWith('### ')) { const t = line.slice(4); out.push('<h3 id="' + __slug(t) + '">' + esc(t) + '</h3>'); }
121
+ else if (line.startsWith('- ')) { if (!inList) { out.push('<ul>'); inList = true; } out.push('<li>' + inl(esc(line.slice(2))) + '</li>'); }
122
+ else { if (inList) { out.push('</ul>'); inList = false; } if (line.trim()) out.push('<p>' + inl(esc(line)) + '</p>'); }
123
+ }
124
+ if (inList) out.push('</ul>');
125
+ if (inCode) out.push('</pre>');
126
+ return out.join('\\n');
127
+ }
128
+
129
+ const bodyNode = data.bodyHtml ? C.Section({ children: h('div', { class: 'page-body', innerHTML: data.bodyHtml }) }) : null;
130
+
131
+ const mainChildren = [
132
+ heroNode(data.hero),
133
+ marqueeNode(data.marquee),
134
+ ...data.sections.map(sectionNode),
135
+ ...(data.panels || []).map(panelNode),
136
+ quickstartNode(data.quickstart),
137
+ examplesNode(data.examples),
138
+ bodyNode,
139
+ ].filter(Boolean);
140
+
141
+ mount(document.getElementById('app'), () => C.AppShell({
142
+ topbar: C.Topbar({ brand: data.siteName, items: data.navItems, active: data.title }),
143
+ crumb: C.Crumb({ leaf: data.title }),
144
+ side: sideNode(data.sidebar),
145
+ main: h('div', { class: 'app-stage' }, ...mainChildren),
146
+ status: C.Status({
147
+ left: data.statusLeft || [data.siteName.toLowerCase(), data.slug],
148
+ right: data.statusRight || ['live'],
149
+ }),
150
+ }));
151
+ `;
@@ -0,0 +1,59 @@
1
+ // Document <head> fragments for the static-site renderer: the full
2
+ // SEO/OG/twitter/schema.org meta block, the favicon link (static href or an
3
+ // inline data: SVG from a single glyph), and the stylesheet link.
4
+
5
+ import { escape } from './markdown.js';
6
+
7
+ // Full SEO/OG/twitter/schema.org meta block, extracted so renderPageHtml
8
+ // consumers (design's own marketing site, thebird's landing) can opt in
9
+ // instead of hand-rolling ~40 lines of <meta> tags per theme.mjs.
10
+ export function renderSeoTags({ title, siteName, seo }) {
11
+ const desc = escape(seo.description || '');
12
+ const url = escape(seo.url || '');
13
+ const image = escape(seo.image || '');
14
+ const author = escape(seo.author || siteName);
15
+ const twitter = escape(seo.twitter || '');
16
+ const locale = escape(seo.locale || 'en_US');
17
+ const keywords = escape(Array.isArray(seo.keywords) ? seo.keywords.join(', ') : (seo.keywords || ''));
18
+ const ogTitle = escape(title);
19
+ const ogDesc = escape(seo.description || siteName);
20
+ let out = `
21
+ <meta name="description" content="${desc}">
22
+ ${keywords ? `<meta name="keywords" content="${keywords}">` : ''}
23
+ <meta name="author" content="${author}">
24
+ <meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1">
25
+ <meta name="generator" content="anentrypoint-design">
26
+ ${url ? `<link rel="canonical" href="${url}">` : ''}
27
+ <meta property="og:type" content="website">
28
+ <meta property="og:title" content="${ogTitle}">
29
+ <meta property="og:description" content="${ogDesc}">
30
+ ${url ? `<meta property="og:url" content="${url}">` : ''}
31
+ <meta property="og:site_name" content="${escape(siteName)}">
32
+ <meta property="og:locale" content="${locale}">
33
+ ${image ? `<meta property="og:image" content="${image}">` : ''}
34
+ <meta name="twitter:card" content="summary_large_image">
35
+ <meta name="twitter:title" content="${ogTitle}">
36
+ <meta name="twitter:description" content="${ogDesc}">
37
+ ${twitter ? `<meta name="twitter:site" content="${twitter}">` : ''}
38
+ ${image ? `<meta name="twitter:image" content="${image}">` : ''}`;
39
+ if (seo.ldJson !== false && (seo.description || url)) {
40
+ const ld = JSON.stringify({
41
+ '@context': 'https://schema.org', '@type': 'WebSite',
42
+ name: title, url: seo.url || '', description: seo.description || '', inLanguage: seo.lang || 'en',
43
+ }).replace(/</g, '\\u003c');
44
+ out += `\n<script type="application/ld+json">${ld}</script>`;
45
+ }
46
+ return out;
47
+ }
48
+
49
+ export function renderFaviconTags({ faviconHref, faviconGlyph }) {
50
+ if (faviconHref) return `<link rel="icon" href="${escape(faviconHref)}">`;
51
+ if (faviconGlyph) return `<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ctext y='26' font-size='26'%3E${encodeURIComponent(faviconGlyph)}%3C/text%3E%3C/svg%3E">`;
52
+ return '';
53
+ }
54
+
55
+ export function renderCssLink({ cssHref, pkgVersion }) {
56
+ return cssHref
57
+ ? `<link rel="stylesheet" href="${cssHref}">`
58
+ : `<link rel="stylesheet" href="https://unpkg.com/anentrypoint-design@${pkgVersion}/dist/247420.css">`;
59
+ }
@@ -0,0 +1,68 @@
1
+ // Server-side markdown for the static-site renderer: HTML escaping, the
2
+ // inline subset, GitHub-flavored heading slugs, and the block renderer with
3
+ // its opt-in ```html raw-passthrough fence.
4
+
5
+ // Single source of HTML escaping lives in markdown.js (full entity set). Kept
6
+ // the `escape` export name for backward compatibility with any consumer.
7
+ import { escapeHtml } from '../markdown.js';
8
+ export const escape = escapeHtml;
9
+
10
+ export function inlineMd(s) {
11
+ return s
12
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
13
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
14
+ .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
15
+ }
16
+
17
+ // GitHub-flavored heading slug: lowercase, strip non-word/non-space/non-hyphen,
18
+ // collapse whitespace to hyphens. Matches the fallback anchor target a hero/nav
19
+ // CTA's `#slug` href expects to resolve against a `## slug text` body heading.
20
+ export function slugify(s) {
21
+ return String(s || '').trim().toLowerCase()
22
+ .replace(/[^\w\s-]/g, '')
23
+ .replace(/\s+/g, '-');
24
+ }
25
+
26
+ // Raw-HTML passthrough block, opt-in via ```html fences. Distinct from a
27
+ // plain ``` code fence (which still escapes+<pre>-wraps its contents) — this
28
+ // is for SSR call sites that are trusted, repo-authored content (a theme.mjs
29
+ // page body sourced from the project's own YAML, never end-user input) and
30
+ // need to emit real markup (e.g. an <iframe> demo embed) that must NOT be
31
+ // escaped. There is no sanitization here by design: the caller owns trust.
32
+ // A consumer rendering untrusted content must not route it through this
33
+ // path — use markdown.js's DOMPurify-backed renderMarkdown for that instead.
34
+ export function renderMarkdown(md) {
35
+ const lines = String(md || '').split('\n');
36
+ const out = [];
37
+ let inCode = false, inList = false, inRawHtml = false;
38
+ for (const line of lines) {
39
+ if (line.trim() === '```html') { if (!inCode) { inRawHtml = true; continue; } }
40
+ if (line.startsWith('```')) {
41
+ if (inRawHtml) { inRawHtml = false; continue; }
42
+ if (inCode) { out.push('</pre>'); inCode = false; } else { out.push('<pre>'); inCode = true; }
43
+ continue;
44
+ }
45
+ if (inRawHtml) { out.push(line); continue; }
46
+ if (inCode) { out.push(escape(line)); continue; }
47
+ if (line.startsWith('# ')) { const t = line.slice(2); out.push(`<h1 id="${slugify(t)}">${escape(t)}</h1>`); }
48
+ else if (line.startsWith('## ')) { const t = line.slice(3); out.push(`<h2 id="${slugify(t)}">${escape(t)}</h2>`); }
49
+ else if (line.startsWith('### ')) { const t = line.slice(4); out.push(`<h3 id="${slugify(t)}">${escape(t)}</h3>`); }
50
+ else if (line.startsWith('- ')) { if (!inList) { out.push('<ul>'); inList = true; } out.push(`<li>${inlineMd(escape(line.slice(2)))}</li>`); }
51
+ else { if (inList) { out.push('</ul>'); inList = false; } if (line.trim()) out.push(`<p>${inlineMd(escape(line))}</p>`); }
52
+ }
53
+ if (inList) out.push('</ul>');
54
+ if (inCode) out.push('</pre>');
55
+ return out.join('\n');
56
+ }
57
+
58
+ // Join a basePath prefix to a nav href. Absolute URLs and hash links pass
59
+ // through unchanged; leading-slash paths get the prefix.
60
+ export function joinHref(basePath, href) {
61
+ if (!href) return '#';
62
+ const h = String(href);
63
+ if (/^([a-z]+:|#|\/\/)/i.test(h)) return h;
64
+ if (!basePath) return h;
65
+ const base = basePath.replace(/\/+$/, '');
66
+ if (h.startsWith('/')) return base + h;
67
+ return base + '/' + h.replace(/^\.?\//, '');
68
+ }
@@ -0,0 +1,44 @@
1
+ // The inline <style> block the SSR document ships ahead of the unpkg CSS
2
+ // bundle: page-stage rhythm, body-prose spacing, hero stat strip, and the
3
+ // feature-row stack. Kept inline (not in a .css file) because it must apply
4
+ // before the bundle loads — several rules carry !important purely to beat the
5
+ // bundle on load-order at equal specificity.
6
+
7
+ export const PAGE_INLINE_STYLES = `
8
+ .app-stage { width: 100%; max-width: var(--stage-wide, min(96vw, 1440px)); margin-inline: auto; padding: var(--space-6, 48px) var(--space-4, 24px) var(--space-8, 96px); display: grid; gap: var(--space-6, 48px); box-sizing: border-box }
9
+ @media (max-width: 768px) { .app-stage { padding: var(--space-4, 24px) var(--space-3, 16px) var(--space-6, 48px); gap: var(--space-5, 32px) } }
10
+ .page-body > :first-child { margin-top: 0 }
11
+ .page-body h1 { margin-top: 0 } .page-body h2 { margin-top: var(--space-5, 32px) } .page-body h3 { margin-top: var(--space-4, 24px) }
12
+ .page-body > * + * { margin-top: var(--space-3, 16px) }
13
+ .page-body pre { margin: var(--space-3, 16px) 0; background: var(--panel-2); padding: var(--space-3, 16px); border-radius: var(--r-1, 10px); overflow-x: auto }
14
+ /* .app-stage owns inter-block rhythm via grid gap; sections/hero must not double it.
15
+ These selectors carry !important because this inline block loads before the
16
+ unpkg CSS bundle, which would otherwise win on load-order for equal specificity. */
17
+ .ds-247420 .app-stage > .ds-hero { margin: 0 !important; padding: var(--space-4, 24px) 0 0 !important; max-width: none !important; gap: var(--space-4, 24px) !important }
18
+ .ds-247420 .app-stage > .ds-section { margin: 0 !important }
19
+ .app-stage .row + .row { margin-top: var(--space-1, 4px) }
20
+ .app-stage .ds-section .row { margin-top: var(--space-2, 8px) }
21
+ .app-stage .ds-section > p.ds-lede { margin: 0 0 var(--space-3, 16px); max-width: var(--measure, 68ch); color: var(--fg-2) }
22
+ .row-benefit { font-style: italic; color: var(--fg-3); font-size: var(--fs-sm); margin-top: var(--space-1, 4px) }
23
+ .ds-row-arrow { margin-left: auto; opacity: .5; transition: opacity var(--dur-snap, 80ms) var(--ease) }
24
+ a.row:hover .ds-row-arrow { opacity: 1 }
25
+ /* hero stat strip — all badges as a wrapping inline rhythm, not one empty panel */
26
+ .ds-hero-stats { display: flex; flex-wrap: wrap; gap: var(--space-3, 16px) var(--space-5, 32px); margin-top: var(--space-2, 8px) }
27
+ .ds-hero-stat { display: flex; align-items: baseline; gap: var(--space-2, 8px) }
28
+ .ds-hero-stat-n { font-family: var(--ff-body); font-weight: 700; font-size: var(--fs-lg, 18px); color: var(--fg) }
29
+ .ds-hero-stat-l { font-size: var(--fs-sm, 15px); color: var(--fg-3) }
30
+ /* accent sits on its own line, muted, so it reads as a distinct aside instead
31
+ of running on from the hero body sentence. */
32
+ .ds-hero-accent { display: block; margin-top: var(--space-2, 8px); color: var(--fg-3) }
33
+ /* feature rows — single-column stack (the dashboard .row grid forces a 3-col
34
+ code/title/meta layout that mangles title+desc+benefit) */
35
+ /* background uses a theme-neutral panel token (resolves per data-theme) so dark
36
+ mode doesn't flash a literal white card before/independent of the bundle.
37
+ Flat tonal fill only — no border-left rail accent (house style: no bespoke
38
+ tile chrome, no shadows, no borders; see ui_kits/gallery/app.js). */
39
+ .ds-feature { padding: var(--space-3, 16px) var(--space-4, 24px); background: var(--panel-1, var(--bg)); border-radius: var(--r-2, 14px); display: grid; gap: var(--space-1, 4px) }
40
+ .ds-feature + .ds-feature { margin-top: var(--space-2, 8px) }
41
+ .ds-feature-title { font-weight: 600; font-size: var(--fs-lg, 18px); color: var(--fg) }
42
+ .ds-feature-desc { font-size: var(--fs-sm, 15px); color: var(--fg-2); line-height: 1.5; overflow-wrap: anywhere }
43
+ .ds-feature-benefit { font-style: italic; font-size: var(--fs-sm, 15px); color: var(--fg-3); margin-top: var(--space-1, 4px) }
44
+ `.trim();