@work-graph/cli 0.2.7 → 0.2.9

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.
@@ -8,16 +8,26 @@ import {
8
8
  buildLlmsTxt,
9
9
  buildMcpDiscovery,
10
10
  getLocalizedFaq,
11
+ getPublicSiteCompetitors,
11
12
  getPublicSiteCopy,
12
13
  getPublicSitePage,
13
14
  renderBvcExample,
14
15
  renderPublicDocMarkdown,
15
16
  } from './publicSiteContent.mjs';
17
+ import {
18
+ localeFromPathname,
19
+ renderPublicSiteBootstrapScript,
20
+ renderPublicSiteControlsScript,
21
+ stripLocalePathPrefix,
22
+ withLocalePath,
23
+ } from './publicSitePreferences.mjs';
16
24
  import { renderUiBadge, UI_BADGE_CSS } from './ui/atoms/badge.mjs';
17
25
  import { renderUiButton, UI_BUTTON_CSS } from './ui/atoms/button.mjs';
26
+ import { highlightBvcBlock, highlightMcpFlow } from './codeSyntaxHighlight.mjs';
18
27
  import { renderInlineIcon, renderThemeIcon } from './ui/iconAssets.mjs';
19
28
 
20
29
  const PUBLIC_SITE_SCHEMA = 'workgraph.public-site.v1';
30
+ const PUBLIC_SITE_GITHUB_URL = 'https://github.com/bvc-lang/work-graph';
21
31
 
22
32
  function escapeHtml(value) {
23
33
  return String(value ?? '')
@@ -39,7 +49,23 @@ function normalizeTheme(value) {
39
49
  }
40
50
 
41
51
  function publicSiteJsonLd(page) {
42
- if (page.kind === 'faq') return buildFaqJsonLd(page.locale);
52
+ if (page.kind === 'home') {
53
+ const faqLd = buildFaqJsonLd(page.locale);
54
+ return {
55
+ '@context': 'https://schema.org',
56
+ '@graph': [
57
+ {
58
+ '@type': 'SoftwareApplication',
59
+ name: page.title,
60
+ description: page.description,
61
+ applicationCategory: 'DeveloperApplication',
62
+ softwareHelp: '/docs',
63
+ codeRepository: 'local-git-workspace',
64
+ },
65
+ { '@type': faqLd['@type'], mainEntity: faqLd.mainEntity },
66
+ ],
67
+ };
68
+ }
43
69
  return {
44
70
  '@context': 'https://schema.org',
45
71
  '@type': page.kind === 'doc' ? 'TechArticle' : 'SoftwareApplication',
@@ -51,73 +77,195 @@ function publicSiteJsonLd(page) {
51
77
  };
52
78
  }
53
79
 
54
- function withLangAndTheme(href, locale, theme) {
55
- if (href.startsWith('#') || href.endsWith('.txt') || href.includes('.well-known')) return href;
56
- const separator = href.includes('?') ? '&' : '?';
57
- return `${href}${separator}lang=${locale}&theme=${theme}`;
58
- }
59
-
60
80
  function icon(name, size = 18) {
61
81
  return renderInlineIcon(`${name}-bold.svg`, { className: 'site-icon', size });
62
82
  }
63
83
 
64
- function renderThemeLocaleControls(locale, theme) {
84
+ const STEP_NUMBER_ICON_NAMES = [
85
+ 'number-one',
86
+ 'number-two',
87
+ 'number-three',
88
+ 'number-four',
89
+ 'number-five',
90
+ 'number-six',
91
+ ];
92
+
93
+ function renderStepNumberIcon(index) {
94
+ const name = STEP_NUMBER_ICON_NAMES[index] ?? 'number-one';
95
+ return renderInlineIcon(`${name}-fill.svg`, { className: 'step-number-icon-svg', size: 32 }, 'fill');
96
+ }
97
+
98
+ function featureIcon(name) {
99
+ return renderInlineIcon(`${name}-fill.svg`, { className: 'feature-column-icon-svg', size: 40 }, 'fill');
100
+ }
101
+
102
+ function comparisonStripIcon(name) {
103
+ return renderInlineIcon(`${name}-fill.svg`, { className: 'comparison-strip-icon-svg', size: 40 }, 'fill');
104
+ }
105
+
106
+ function workflowPipelineIcon(name) {
107
+ return renderInlineIcon(`${name}-fill.svg`, { className: 'workflow-pipeline-icon-svg', size: 32 }, 'fill');
108
+ }
109
+
110
+ function renderFeatureColumns({ title, items }) {
111
+ return `<section class="feature-columns-section">
112
+ <h2 class="feature-columns-heading">${escapeHtml(title)}</h2>
113
+ <div class="feature-columns">${items.map(({ iconName, heading, body }) => `<article class="feature-column">
114
+ <span class="feature-column-icon" aria-hidden="true">${featureIcon(iconName)}</span>
115
+ <h3>${escapeHtml(heading)}</h3>
116
+ <p>${escapeHtml(body)}</p>
117
+ </article>`).join('')}</div>
118
+ </section>`;
119
+ }
120
+
121
+ function renderIconLabelGridIcon() {
122
+ return renderInlineIcon('check-bold.svg', { className: 'icon-label-grid-icon-svg', size: 22 }, 'bold');
123
+ }
124
+
125
+ /** Full-viewport background band; content stays in __inner max-width column. */
126
+ function wrapSiteSectionBand(content, { tone = 'muted', innerClass = '' } = {}) {
127
+ const innerAttr = innerClass ? ` ${innerClass}` : '';
128
+ return `<div class="site-section-band site-section-band--${tone}">
129
+ <div class="site-section-band__inner${innerAttr}">${content}</div>
130
+ </div>`;
131
+ }
132
+
133
+ function renderIconLabelGrid({ title, body, items }) {
134
+ return `<section class="icon-label-grid-section">
135
+ <div class="icon-label-grid-heading">
136
+ <h2>${escapeHtml(title)}</h2>
137
+ ${body ? `<p>${escapeHtml(body)}</p>` : ''}
138
+ </div>
139
+ <div class="icon-label-grid">${items.map((label) => `<div class="icon-label-grid-item">
140
+ <span class="icon-label-grid-icon" aria-hidden="true"><span class="icon-label-grid-icon-box">${renderIconLabelGridIcon()}</span></span>
141
+ <span class="icon-label-grid-label">${escapeHtml(label)}</span>
142
+ </div>`).join('')}</div>
143
+ </section>`;
144
+ }
145
+
146
+ function renderThemeToggleIcons() {
147
+ const moon = renderThemeIcon('moon', { className: 'site-icon theme-icon theme-icon-moon', size: 18 });
148
+ const sun = renderThemeIcon('sun', { className: 'site-icon theme-icon theme-icon-sun', size: 18 });
149
+ return `<span class="theme-toggle-icons" aria-hidden="true">${moon}${sun}</span>`;
150
+ }
151
+
152
+ function renderNavToggle(locale) {
65
153
  const copy = getPublicSiteCopy(locale);
66
- const nextTheme = theme === 'dark' ? 'light' : 'dark';
67
- const localeLinks = PUBLIC_SITE_LOCALES.map((candidate) => (
68
- `<a class="locale-link${candidate === locale ? ' is-active' : ''}" href="?lang=${candidate}&theme=${theme}" hreflang="${candidate}" data-locale-value="${candidate}">${candidate.toUpperCase()}</a>`
69
- )).join('');
70
- return `<div class="site-controls" aria-label="${escapeAttr(copy.localeLabel)}">
71
- <a class="theme-toggle wg-btn wg-btn--secondary wg-btn--sm" href="?lang=${locale}&theme=${nextTheme}" data-theme-toggle data-theme-value="${nextTheme}">${renderThemeIcon(nextTheme === 'dark' ? 'moon' : 'sun')} ${escapeHtml(copy.theme[nextTheme])}</a>
72
- <span class="locale-links">${localeLinks}</span>
154
+ return `<button type="button" class="site-nav-toggle site-control-btn" data-nav-toggle aria-controls="site-nav" aria-expanded="false" aria-label="${escapeAttr(copy.nav.menuOpen)}" data-label-open="${escapeAttr(copy.nav.menuOpen)}" data-label-close="${escapeAttr(copy.nav.menuClose)}"><span class="site-nav-toggle-bars" aria-hidden="true"></span></button>`;
155
+ }
156
+
157
+ function renderHeaderGithubButton(locale) {
158
+ const label = locale === 'ru' ? 'GitHub репозиторий Work Graph' : 'Work Graph on GitHub';
159
+ const icon = renderInlineIcon('github-logo-fill.svg', { className: 'site-github-icon-svg', size: 20 }, 'fill');
160
+ return `<a class="site-control-btn site-github-btn" href="${escapeAttr(PUBLIC_SITE_GITHUB_URL)}" target="_blank" rel="noopener noreferrer" aria-label="${escapeAttr(label)}">${icon}</a>`;
161
+ }
162
+
163
+ function renderThemeLocaleControls(locale, theme) {
164
+ const localeLabel = locale === 'ru' ? 'En' : 'Ru';
165
+ const themeAriaDark = locale === 'ru' ? 'Тёмная тема' : 'Dark theme';
166
+ const themeAriaLight = locale === 'ru' ? 'Светлая тема' : 'Light theme';
167
+ const localeAria = locale === 'ru' ? 'English' : 'Русский';
168
+ return `<div class="site-controls" aria-label="${escapeAttr(getPublicSiteCopy(locale).localeLabel)}">
169
+ <button type="button" class="site-control-btn theme-toggle" data-theme-toggle data-label-dark="${escapeAttr(themeAriaDark)}" data-label-light="${escapeAttr(themeAriaLight)}" aria-label="${escapeAttr(theme === 'dark' ? themeAriaLight : themeAriaDark)}">${renderThemeToggleIcons()}</button>
170
+ <button type="button" class="site-control-btn locale-toggle" data-locale-toggle aria-label="${escapeAttr(localeAria)}">${escapeHtml(localeLabel)}</button>
73
171
  </div>`;
74
172
  }
75
173
 
76
- function renderNav(locale, theme) {
174
+ function renderSiteBrand(locale) {
175
+ const home = withLocalePath('/', locale);
176
+ const label = 'Work Graph';
177
+ return `<a class="site-brand" href="${escapeAttr(home)}" aria-label="${escapeAttr(label)}">
178
+ <img class="site-brand-logo" src="/assets/workgraph-logo.svg" width="188" height="24" alt="${escapeAttr(label)}" decoding="async">
179
+ <img class="site-brand-emblem" src="/assets/workgraph-emblem.svg" width="41" height="24" alt="" aria-hidden="true" decoding="async">
180
+ </a>`;
181
+ }
182
+
183
+ function isNavLinkActive(href, activeRoute) {
184
+ if (href === '/') return activeRoute === '/';
185
+ if (href === '/docs') return activeRoute === '/docs' || activeRoute.startsWith('/docs/');
186
+ return activeRoute === href;
187
+ }
188
+
189
+ function renderNav(locale, activeRoute = '/') {
77
190
  const copy = getPublicSiteCopy(locale);
78
191
  const links = [
79
- ['/', 'Work Graph'],
80
192
  ['/product', copy.nav.product],
81
193
  ['/evidence-ledger', copy.nav.evidence],
82
194
  ['/compare', copy.nav.compare],
83
- ['/faq', 'FAQ'],
84
195
  ['/docs', copy.nav.docs],
85
196
  ];
86
- return `<nav class="site-nav" aria-label="Work Graph public navigation">
87
- ${links.map(([href, label]) => `<a href="${withLangAndTheme(href, locale, theme)}">${escapeHtml(label)}</a>`).join('')}
197
+ return `<nav class="site-nav" id="site-nav" aria-label="Work Graph public navigation">
198
+ ${links.map(([href, label]) => {
199
+ const active = isNavLinkActive(href, activeRoute);
200
+ const current = active ? ' aria-current="page"' : '';
201
+ return `<a href="${withLocalePath(href, locale)}"${current}>${escapeHtml(label)}</a>`;
202
+ }).join('')}
88
203
  </nav>`;
89
204
  }
90
205
 
206
+ const SCREENSHOT_KEY_INDEX = {
207
+ analytics: 0,
208
+ tasks: 1,
209
+ board: 2,
210
+ verification: 3,
211
+ memory: 4,
212
+ architecture: 5,
213
+ };
214
+
91
215
  const SCREENSHOTS = [
92
- {
93
- src: '/assets/img/work-graph-kanban-board-light.png',
94
- title: { en: 'Kanban board', ru: 'Доска задач' },
95
- body: { en: 'Local backlog columns with BVC work items and agent ownership.', ru: 'Локальная доска с BVC-задачами и владельцами.' },
96
- },
97
216
  {
98
217
  src: '/assets/img/work-graph-analytics-list.png',
99
- title: { en: 'Analytics list', ru: 'Аналитика' },
100
- body: { en: 'Decision and research records connected to implementation work.', ru: 'Решения и исследования, связанные с задачами реализации.' },
218
+ title: { en: 'Analytics', ru: 'Аналитика' },
219
+ headline: { en: 'Analytics links decisions to delivery work', ru: 'Аналитика связывает решения с задачами реализации' },
220
+ body: {
221
+ en: 'AN records capture reasoning, options and boundaries before work enters the backlog. Lineage, epic links and implementation ties stay in the intent graph inside git — not in a separate doc or chat summary.',
222
+ ru: 'AN-записи фиксируют аргументацию, варианты и границы до появления work items. Видны lineage, связи с эпиками и реализацией — не отдельный документ, а часть графа намерений в репозитории.',
223
+ },
101
224
  },
102
225
  {
103
226
  src: '/assets/img/work-graph-task-drawer.png',
104
- title: { en: 'Task contract drawer', ru: 'Контракт задачи' },
105
- body: { en: 'Basis, Vector, Goal, analysis, decisions and evidence in one drawer.', ru: 'Базис, Вектор, Цель, анализ, решения и evidence в одной панели.' },
227
+ title: { en: 'Tasks', ru: 'Задачи' },
228
+ headline: { en: 'A task is a machine-readable BVC contract', ru: 'Задача машиночитаемый BVC-контракт, а не тикет из чата' },
229
+ body: {
230
+ en: 'The drawer shows Basis, Vector, Goal, analysis, decisions, checks and evidence in one place. Agents read projection via get_work_contract and know which files, commands and gates are required before done.',
231
+ ru: 'В панели задачи: Базис, Вектор, Цель, анализ, решения, проверки и evidence. Агент читает projection через get_work_contract и знает, какие файлы, команды и гейты обязательны до закрытия.',
232
+ },
233
+ },
234
+ {
235
+ src: '/assets/img/work-graph-kanban-board-light.png',
236
+ title: { en: 'Kanban board', ru: 'Доска задач' },
237
+ headline: { en: 'The board shows how work is moving', ru: 'Доска показывает, как движется работа' },
238
+ body: {
239
+ en: 'Columns from backlog through ready, doing and done with BVC work items and owners. Status follows contract and evidence — not an arbitrary label someone typed in a thread.',
240
+ ru: 'Колонки от backlog до ready, doing и done с BVC-задачами и владельцами. Статус — следствие контракта и evidence, а не произвольная метка в переписке.',
241
+ },
106
242
  },
107
243
  {
108
244
  src: '/assets/img/work-graph-verification-matrix.png',
109
- title: { en: 'Verification matrix', ru: 'Матрица проверок' },
110
- body: { en: 'Deterministic, optional and environment-dependent gates before done.', ru: 'Детерминированные, опциональные и environment-гейты перед done.' },
245
+ title: { en: 'Verification', ru: 'Проверки' },
246
+ headline: { en: 'Verification decides when a task can close', ru: 'Проверки решают, можно ли закрыть задачу' },
247
+ body: {
248
+ en: 'Tier A/B/C matrix: deterministic commands, optional checks and environment gates. assert_task_ready_for_done returns violations[] — a contract verdict, not agent prose.',
249
+ ru: 'Матрица tier A/B/C: детерминированные команды, опциональные проверки и environment-гейты. assert_task_ready_for_done возвращает violations[] — вердикт контракта, а не слова агента.',
250
+ },
111
251
  },
112
252
  {
113
- src: '/assets/img/work-graph-architecture-drawer.png',
114
- title: { en: 'Architecture drawer', ru: 'Архитектура' },
115
- body: { en: 'Architecture blocks and derived projections for project navigation.', ru: 'Архитектурные блоки и производные проекции для навигации.' },
253
+ src: '/assets/img/work-graph-memory-list.png',
254
+ title: { en: 'Project memory', ru: 'Память проекта' },
255
+ headline: { en: 'Project memory keeps verified outcomes', ru: 'Память проекта хранит проверенные результаты' },
256
+ body: {
257
+ en: 'Closed tasks with valid evidence become memory records linked to work.id and files. The next session pulls context from git, not from a recap of what the model said last time.',
258
+ ru: 'Закрытые задачи с валидным evidence становятся записями памяти со ссылками на work.id и файлы. Следующая сессия опирается на git, а не на пересказ прошлого чата.',
259
+ },
116
260
  },
117
261
  {
118
- src: '/assets/img/work-graph-kanban-board-dark.png',
119
- title: { en: 'Dark mode', ru: 'Тёмная тема' },
120
- body: { en: 'The same local board in dark mode.', ru: 'Та же локальная доска в тёмной теме.' },
262
+ src: '/assets/img/work-graph-architecture-drawer.png',
263
+ title: { en: 'Architecture', ru: 'Архитектура' },
264
+ headline: { en: 'Architecture orients you in a large repo', ru: 'Архитектура помогает ориентироваться в большом репозитории' },
265
+ body: {
266
+ en: 'Blocks from architecture/main.bvc and derived projections map domains and containers without breaking away from the intent graph and backlog you execute against.',
267
+ ru: 'Блоки architecture/main.bvc и производные проекции показывают домены и контейнеры без отрыва от intent-графа и бэклога, по которому идёт работа.',
268
+ },
121
269
  },
122
270
  ];
123
271
 
@@ -133,7 +281,7 @@ function renderSection(section, copy) {
133
281
  ? `<ul class="doc-list">${section.docs.map((doc) => `<li><a href="/docs/${escapeAttr(doc.slug)}">${escapeHtml(doc.title)}</a><p>${escapeHtml(doc.description)}</p></li>`).join('')}</ul>`
134
282
  : '';
135
283
  const competitors = section.competitors
136
- ? `<table><thead><tr><th>${escapeHtml(copy.labels.tableCompetitor)}</th><th>${escapeHtml(copy.labels.tableLayer)}</th><th>${escapeHtml(copy.labels.tableEvidence)}</th><th>${escapeHtml(copy.labels.tableStance)}</th></tr></thead><tbody>${section.competitors.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table>`
284
+ ? `<div class="table-scroll" tabindex="0"><table><thead><tr><th>${escapeHtml(copy.labels.tableCompetitor)}</th><th>${escapeHtml(copy.labels.tableLayer)}</th><th>${escapeHtml(copy.labels.tableEvidence)}</th><th>${escapeHtml(copy.labels.tableStance)}</th></tr></thead><tbody>${section.competitors.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`
137
285
  : '';
138
286
  const badges = section.badges
139
287
  ? `<div class="badge-row">${section.badges.map((badge) => renderUiBadge(badge)).join('')}</div>`
@@ -147,142 +295,196 @@ function renderSection(section, copy) {
147
295
  </section>`;
148
296
  }
149
297
 
150
- function renderHeroVisual(locale) {
298
+ function renderHeroVisual(locale, theme) {
299
+ const lightSrc = '/assets/img/work-graph-kanban-board-light.png';
300
+ const darkSrc = '/assets/img/work-graph-kanban-board-dark.png';
301
+ const src = theme === 'dark' ? darkSrc : lightSrc;
151
302
  return `<figure class="template-visual screenshot-hero" aria-label="Work Graph board preview">
152
- <img src="/assets/img/work-graph-kanban-board-light.png" alt="${escapeAttr(locale === 'ru' ? 'Доска Work Graph' : 'Work Graph kanban board')}" loading="eager" decoding="async">
303
+ <img src="${escapeAttr(src)}" data-hero-screenshot data-light-src="${escapeAttr(lightSrc)}" data-dark-src="${escapeAttr(darkSrc)}" alt="${escapeAttr(locale === 'ru' ? 'Доска Work Graph' : 'Work Graph kanban board')}" loading="eager" decoding="async">
153
304
  <figcaption>${escapeHtml(locale === 'ru' ? 'Локальная доска Work Graph: backlog, ready, in progress and done.' : 'Local Work Graph board: backlog, ready, in progress and done.')}</figcaption>
154
305
  </figure>`;
155
306
  }
156
307
 
157
- function renderScreenshotGallery(locale) {
158
- return `<section class="screenshot-gallery" aria-label="${escapeAttr(locale === 'ru' ? 'Скриншоты Work Graph' : 'Work Graph screenshots')}">
159
- <div class="wide-heading">
160
- <p class="eyebrow">${escapeHtml(locale === 'ru' ? 'Интерфейс' : 'Interface')}</p>
161
- <h2>${escapeHtml(locale === 'ru' ? 'Как выглядит Work Graph' : 'What Work Graph looks like')}</h2>
162
- <p>${escapeHtml(locale === 'ru' ? 'Реальные экраны локального UI: доска, аналитика, контракты задач, проверки и архитектура.' : 'Real local UI screens: board, analytics, task contracts, verification and architecture.')}</p>
163
- </div>
164
- <div class="screenshot-grid">
165
- ${SCREENSHOTS.map((shot) => `<figure class="screenshot-card">
166
- <img src="${escapeAttr(shot.src)}" alt="${escapeAttr(screenshotText(shot.title, locale))}" loading="lazy" decoding="async">
167
- <figcaption>
168
- <strong>${escapeHtml(screenshotText(shot.title, locale))}</strong>
169
- <span>${escapeHtml(screenshotText(shot.body, locale))}</span>
170
- </figcaption>
171
- </figure>`).join('')}
172
- </div>
173
- </section>`;
308
+ function resolveScreenshotList(shotKeys) {
309
+ if (!shotKeys?.length) return SCREENSHOTS;
310
+ return shotKeys
311
+ .map((key) => SCREENSHOTS[SCREENSHOT_KEY_INDEX[key]])
312
+ .filter(Boolean);
174
313
  }
175
314
 
176
- function renderTemplateAside(copy, locale, theme) {
177
- const title = locale === 'ru' ? 'Шаблон доказуемой разработки' : 'Evidence-led development template';
178
- const meta = locale === 'ru'
179
- ? ['Локально в git', 'BVC-контракты', 'MCP-ready', 'Без БД']
180
- : ['Local git', 'BVC contracts', 'MCP-ready', 'No database'];
181
- return `<aside class="template-aside" aria-label="${escapeAttr(title)}">
182
- <div class="aside-card">
183
- <div class="aside-icon">${icon('kanban', 22)}</div>
184
- <h2>${escapeHtml(title)}</h2>
185
- <p>${escapeHtml(copy.hero.body)}</p>
186
- ${renderUiButton({ href: '#install', label: copy.hero.primary, variant: 'primary', size: 'sm' })}
187
- <dl>${meta.map((entry) => `<div><dt>${escapeHtml(entry)}</dt><dd>${renderUiBadge({ label: 'WG', tone: 'accent' })}</dd></div>`).join('')}</dl>
315
+ function renderScreenshotGallery(locale, options = {}) {
316
+ const shots = resolveScreenshotList(options.shotKeys);
317
+ const tablistLabel = locale === 'ru' ? 'Экраны Work Graph' : 'Work Graph screens';
318
+ const tabs = shots.map((shot, index) => {
319
+ const id = `screenshot-${index}`;
320
+ const label = screenshotText(shot.title, locale);
321
+ const active = index === 0;
322
+ return `<button type="button" class="screenshot-tab${active ? ' is-active' : ''}" role="tab" id="${id}-tab" data-screenshot-tab="${id}" aria-selected="${active ? 'true' : 'false'}" aria-controls="${id}-panel">${escapeHtml(label)}</button>`;
323
+ }).join('');
324
+ const panels = shots.map((shot, index) => {
325
+ const id = `screenshot-${index}`;
326
+ const title = screenshotText(shot.title, locale);
327
+ const headline = screenshotText(shot.headline ?? shot.title, locale);
328
+ const body = screenshotText(shot.body, locale);
329
+ const active = index === 0;
330
+ return `<article class="screenshot-panel${active ? ' is-active' : ''}" role="tabpanel" id="${id}-panel" data-screenshot-panel="${id}" aria-labelledby="${id}-tab"${active ? '' : ' hidden'}>
331
+ <div class="screenshot-panel-copy">
332
+ <h3>${escapeHtml(headline)}</h3>
333
+ <p>${escapeHtml(body)}</p>
334
+ </div>
335
+ <div class="screenshot-panel-visual">
336
+ <div class="screenshot-panel-frame">
337
+ <img src="${escapeAttr(shot.src)}" alt="${escapeAttr(title)}" loading="${active ? 'eager' : 'lazy'}" decoding="async">
338
+ </div>
339
+ </div>
340
+ </article>`;
341
+ }).join('');
342
+ const sectionIntro = options.lead ?? (locale === 'ru'
343
+ ? 'Локальный UI Work Graph ведёт полный цикл: от аналитического разбора и BVC-задач до доски, проверок, памяти и архитектуры. Переключайте экраны и смотрите, как решения, контракты и доказательства связаны в одном репозитории.'
344
+ : 'The local Work Graph UI runs the full loop: from analytics review and BVC tasks to the board, verification, memory and architecture. Switch screens to see how decisions, contracts and evidence connect in one repository.');
345
+ const galleryTitle = options.title ?? (locale === 'ru' ? 'Как выглядит Work Graph' : 'What Work Graph looks like');
346
+ const gallery = `<section class="screenshot-gallery" aria-label="${escapeAttr(locale === 'ru' ? 'Скриншоты Work Graph' : 'Work Graph screenshots')}">
347
+ <div class="screenshot-gallery-heading">
348
+ <h2>${escapeHtml(galleryTitle)}</h2>
349
+ <p class="screenshot-gallery-lead">${escapeHtml(sectionIntro)}</p>
188
350
  </div>
189
- </aside>`;
351
+ <div class="screenshot-switcher" data-screenshot-switcher>
352
+ <div class="screenshot-tablist" role="tablist" aria-label="${escapeAttr(tablistLabel)}">${tabs}</div>
353
+ <div class="screenshot-panels">${panels}</div>
354
+ </div>
355
+ </section>`;
356
+ return wrapSiteSectionBand(gallery, { tone: 'muted' });
190
357
  }
191
358
 
192
359
  function renderSteps(copy, locale) {
193
360
  const steps = locale === 'ru'
194
361
  ? [
195
- ['Зафиксируйте решение', 'Создайте AN-разбор: почему работа нужна, какие риски и где границы.'],
196
- ['Добавьте контракт задачи', 'Переведите решение в BVC-атом с Базисом, Вектором, Целью и проверками.'],
197
- ['Назначьте агента', 'Cursor, Claude Code или другой MCP-клиент берёт задачу, но не становится источником правды.'],
198
- ['Приложите доказательства', 'Команды, файлы, проверки и доменный контекст попадают в evidence.'],
199
- ['Сохраните память', 'После готовности результат становится проверенной памятью проекта.'],
362
+ ['Установите Work Graph в проект', 'Выполните npx @work-graph/cli init . и npm install — появятся intent/, конфиг и MCP для Cursor или другого клиента.'],
363
+ ['Исследуйте вопрос', 'Попросите ИИ провести аналитический разбор он оформит его как AN-запись в проекте.'],
364
+ ['Добавьте задачи', 'Попросите ИИ на основе разбора создать задачи или целый эпик с BVC-контрактами.'],
365
+ ['Выполнение', 'Агент захватывает work.id, меняет код в рамках контракта и прикладывает evidence; статусы видны на доске.'],
366
+ ['Проверки', 'Детерминированные и опциональные гейты решают, можно ли закрыть задачу — не слова агента, а контракт.'],
367
+ ['Память', 'После готовности проверенный результат становится памятью проекта со связями к задачам и файлам.'],
200
368
  ]
201
369
  : [
202
- ['Capture the decision', 'Write an AN analysis: why the work matters, what can go wrong and where the boundary is.'],
203
- ['Add the work contract', 'Turn the decision into a BVC atom with Basis, Vector, Goal and checks.'],
204
- ['Assign the agent', 'Cursor, Claude Code or another MCP client can execute without becoming the source of truth.'],
205
- ['Attach evidence', 'Commands, files, checks and domain context become task evidence.'],
206
- ['Preserve memory', 'After readiness, the outcome becomes verified project memory.'],
370
+ ['Install Work Graph in your repo', 'Run npx @work-graph/cli init . and npm install you get intent/, config, and MCP for Cursor or another client.'],
371
+ ['Explore the question', 'Ask the agent for an analytics review it records the outcome as an AN entry in the project.'],
372
+ ['Add work items', 'Ask the agent to create tasks or a full epic from the review, each with a BVC contract.'],
373
+ ['Execution', 'The agent claims a work.id, edits code within the contract, and attaches evidence; the board shows progress.'],
374
+ ['Verification', 'Deterministic and optional gates decide when a task can close — contract verdict, not agent prose.'],
375
+ ['Memory', 'After readiness, the verified outcome becomes project memory linked to work items and files.'],
207
376
  ];
208
377
  return `<section class="steps-section">
209
378
  <h2>${escapeHtml(locale === 'ru' ? 'Как начать с Work Graph' : 'How to get started with Work Graph')}</h2>
210
379
  <ol class="jira-steps">${steps.map(([title, body], index) => `<li>
211
- <span class="step-number">${index + 1}</span>
212
- <div><h3>${escapeHtml(title)}</h3><p>${escapeHtml(body)}</p></div>
380
+ <span class="step-number" aria-hidden="true">${renderStepNumberIcon(index)}</span>
381
+ <h3><strong>${escapeHtml(title)}</strong> ${escapeHtml(body)}</h3>
213
382
  </li>`).join('')}</ol>
214
383
  </section>`;
215
384
  }
216
385
 
217
- function renderRelatedTemplates(locale, theme) {
218
- const cards = locale === 'ru'
219
- ? [
220
- ['Контракт задачи', 'BVC-атом с Базисом, Вектором, Целью и проверками.', '/docs/bvc-spec'],
221
- ['Матрица проверок', 'Tier A/B/C readiness для работы агентов.', '/docs/verification-matrix'],
222
- ['MCP-инструменты', 'Контракты tools для Cursor и Claude Code.', '/docs/mcp-tools'],
223
- ]
224
- : [
225
- ['Work contract', 'BVC atom with Basis, Vector, Goal and checks.', '/docs/bvc-spec'],
226
- ['Verification matrix', 'Tier A/B/C readiness for agent work.', '/docs/verification-matrix'],
227
- ['MCP tools', 'Tool contracts for Cursor and Claude Code.', '/docs/mcp-tools'],
228
- ];
229
- return `<section class="related-templates">
230
- <div class="related-inner">
231
- <h2>${escapeHtml(locale === 'ru' ? 'Связанные шаблоны' : 'Related templates')}</h2>
232
- <div class="related-grid">${cards.map(([title, body, href]) => `<article class="related-card">
233
- <div class="related-preview"><span></span><span></span><span></span></div>
234
- <h3>${escapeHtml(title)}</h3>
235
- <p>${escapeHtml(body)}</p>
236
- <a href="${withLangAndTheme(href, locale, theme)}">${escapeHtml(locale === 'ru' ? 'Открыть →' : 'Open →')}</a>
237
- </article>`).join('')}</div>
238
- </div>
239
- </section>`;
240
- }
241
-
242
386
  function renderBottomCta(copy, locale, theme) {
387
+ const installHref = `${withLocalePath('/', locale)}#install`;
243
388
  return `<section class="bottom-cta">
244
- <h2>${escapeHtml(locale === 'ru' ? 'Готовы поставить Work Graph локально?' : 'Ready to install Work Graph locally?')}</h2>
245
- ${renderUiButton({ href: '#install', label: copy.hero.primary, variant: 'primary', size: 'sm' })}
389
+ <div class="bottom-cta__inner">
390
+ <h2>${escapeHtml(locale === 'ru' ? 'Готовы поставить Work Graph локально?' : 'Ready to install Work Graph locally?')}</h2>
391
+ ${renderUiButton({ href: installHref, label: copy.hero.primary, variant: 'primary', size: 'lg' })}
392
+ </div>
246
393
  </section>`;
247
394
  }
248
395
 
249
- function renderFooterColumns(locale) {
396
+ function renderFooter(locale) {
397
+ const year = new Date().getFullYear();
398
+ const home = withLocalePath('/', locale);
399
+ const brandLinks = locale === 'ru'
400
+ ? [
401
+ ['https://github.com/bvc-lang/work-graph', 'GitHub'],
402
+ [withLocalePath('/docs', locale), 'Документация'],
403
+ ['/llms.txt', 'llms.txt'],
404
+ ]
405
+ : [
406
+ ['https://github.com/bvc-lang/work-graph', 'GitHub'],
407
+ [withLocalePath('/docs', locale), 'Documentation'],
408
+ ['/llms.txt', 'llms.txt'],
409
+ ];
250
410
  const columns = locale === 'ru'
251
411
  ? [
252
- ['Продукт', ['Аналитика', 'Задачи', 'Доска', 'Проверки']],
253
- ['Документация', ['BVC', 'MCP', 'Проверки', 'Ошибки']],
254
- ['Для агентов', ['llms.txt', 'Markdown', 'MCP discovery', 'JSON contexts']],
255
- ['Сравнение', ['Cursor', 'Linear', 'Mem0', 'Devin']],
412
+ ['Продукт', [
413
+ [withLocalePath('/product', locale), 'Аналитика'],
414
+ [withLocalePath('/product', locale), 'Задачи BVC'],
415
+ [withLocalePath('/product', locale), 'Доска'],
416
+ [withLocalePath('/evidence-ledger', locale), 'Проверки'],
417
+ ]],
418
+ ['Документация', [
419
+ [withLocalePath('/docs/bvc-spec', locale), 'BVC'],
420
+ [withLocalePath('/docs/mcp-tools', locale), 'MCP'],
421
+ [withLocalePath('/docs/verification-matrix', locale), 'Матрица проверок'],
422
+ [withLocalePath('/docs', locale), 'Все документы'],
423
+ ]],
424
+ ['Для агентов', [
425
+ ['/llms.txt', 'llms.txt'],
426
+ [withLocalePath('/docs', locale), 'Markdown'],
427
+ ['/.well-known/mcp.json', 'MCP discovery'],
428
+ [withLocalePath('/docs', locale), 'JSON contexts'],
429
+ ]],
256
430
  ]
257
431
  : [
258
- ['Product', ['Analytics', 'Work items', 'Board', 'Verification']],
259
- ['Docs', ['BVC', 'MCP', 'Verification', 'Errors']],
260
- ['For agents', ['llms.txt', 'Markdown', 'MCP discovery', 'JSON contexts']],
261
- ['Compare', ['Cursor', 'Linear', 'Mem0', 'Devin']],
432
+ ['Product', [
433
+ [withLocalePath('/product', locale), 'Analytics'],
434
+ [withLocalePath('/product', locale), 'Work items'],
435
+ [withLocalePath('/product', locale), 'Board'],
436
+ [withLocalePath('/evidence-ledger', locale), 'Verification'],
437
+ ]],
438
+ ['Docs', [
439
+ [withLocalePath('/docs/bvc-spec', locale), 'BVC'],
440
+ [withLocalePath('/docs/mcp-tools', locale), 'MCP'],
441
+ [withLocalePath('/docs/verification-matrix', locale), 'Verification matrix'],
442
+ [withLocalePath('/docs', locale), 'All docs'],
443
+ ]],
444
+ ['For agents', [
445
+ ['/llms.txt', 'llms.txt'],
446
+ [withLocalePath('/docs', locale), 'Markdown'],
447
+ ['/.well-known/mcp.json', 'MCP discovery'],
448
+ [withLocalePath('/docs', locale), 'JSON contexts'],
449
+ ]],
262
450
  ];
263
- return `<div class="footer-columns">${columns.map(([title, items]) => `<div><h3>${escapeHtml(title)}</h3>${items.map((item) => `<a href="/docs">${escapeHtml(item)}</a>`).join('')}</div>`).join('')}</div>`;
264
- }
265
-
266
- function renderProofStats(locale) {
267
- const stats = locale === 'ru'
451
+ const legal = locale === 'ru'
268
452
  ? [
269
- ['100%', 'локально в репозитории'],
270
- ['5', 'шагов от решения до памяти'],
271
- ['3', 'MCP-инструмента для агента'],
272
- ['0', 'обязательных SaaS/БД'],
453
+ [withLocalePath('/docs', locale), 'Документация'],
454
+ ['https://github.com/bvc-lang/work-graph', 'GitHub'],
273
455
  ]
274
456
  : [
275
- ['100%', 'local in the repository'],
276
- ['5', 'steps from decision to memory'],
277
- ['3', 'MCP tools for agents'],
278
- ['0', 'required SaaS/database'],
457
+ [withLocalePath('/docs', locale), 'Documentation'],
458
+ ['https://github.com/bvc-lang/work-graph', 'GitHub'],
279
459
  ];
280
- return `<section class="proof-stats" aria-label="Work Graph facts">
281
- ${stats.map(([value, label]) => `<div><strong>${escapeHtml(value)}</strong><span>${escapeHtml(label)}</span></div>`).join('')}
282
- </section>`;
460
+ const external = (href) => href.startsWith('http');
461
+ return `<footer class="site-footer">
462
+ <div class="site-footer__panel">
463
+ <div class="site-footer__grid">
464
+ <div class="site-footer__brand">
465
+ <a class="site-footer__logo" href="${escapeAttr(home)}" aria-label="Work Graph">
466
+ <img src="/assets/workgraph-logo.svg" width="148" height="22" alt="Work Graph" decoding="async">
467
+ </a>
468
+ <nav class="site-footer__brand-links" aria-label="${escapeAttr(locale === 'ru' ? 'Быстрые ссылки' : 'Quick links')}">
469
+ ${brandLinks.map(([href, label]) => `<a href="${escapeAttr(href)}"${external(href) ? ' target="_blank" rel="noopener noreferrer"' : ''}>${escapeHtml(label)}</a>`).join('')}
470
+ </nav>
471
+ </div>
472
+ <div class="footer-columns">${columns.map(([title, items]) => `<div class="footer-column">
473
+ <h3>${escapeHtml(title)}</h3>
474
+ ${items.map(([href, label]) => `<a href="${escapeAttr(href)}"${external(href) ? ' target="_blank" rel="noopener noreferrer"' : ''}>${escapeHtml(label)}</a>`).join('')}
475
+ </div>`).join('')}</div>
476
+ </div>
477
+ </div>
478
+ <div class="site-footer__bottom">
479
+ <p class="site-footer__copy">${escapeHtml(locale === 'ru' ? `© ${year} Work Graph` : `Copyright © ${year} Work Graph`)}</p>
480
+ <nav class="site-footer__legal" aria-label="${escapeAttr(locale === 'ru' ? 'Правовая информация' : 'Legal')}">
481
+ ${legal.map(([href, label]) => `<a href="${escapeAttr(href)}"${external(href) ? ' target="_blank" rel="noopener noreferrer"' : ''}>${escapeHtml(label)}</a>`).join('')}
482
+ </nav>
483
+ </div>
484
+ </footer>`;
283
485
  }
284
486
 
285
- function renderGraphTrinity(locale) {
487
+ function renderGraphTrinity(locale, options = {}) {
286
488
  const graphs = locale === 'ru'
287
489
  ? [
288
490
  ['Граф намерений', 'Что и зачем', 'BVC-атомы, AN → Epic → Work Item, связи depends_on и trace.*.'],
@@ -294,194 +496,353 @@ function renderGraphTrinity(locale) {
294
496
  ['Execution Graph', 'How and by whom', 'work.id tasks, evidence, todo → ready → doing → done/blocked states and gates.'],
295
497
  ['Memory Graph', 'What was decided and why', 'Closed tasks with valid evidence, audit trail and RAG context from git.'],
296
498
  ];
499
+ const title = options.title ?? (locale === 'ru' ? 'Три графа — один цикл разработки' : 'Three graphs, one development loop');
500
+ const body = options.body ?? (locale === 'ru'
501
+ ? 'Каждый граф — самостоятельный слой с чётким контрактом. Вместе они дают цикл, который агент может исполнять, а человек — аудировать.'
502
+ : 'Each graph is a standalone layer with a clear contract. Together they form a loop an agent can execute and a human can audit.');
503
+ const cards = graphs.map(([graphTitle, subtitle, graphBody], index) => `<article class="graph-flow-card">
504
+ <span class="graph-flow-step" aria-hidden="true">${index + 1}</span>
505
+ <h3 class="graph-flow-card-title">${escapeHtml(graphTitle)}</h3>
506
+ <p class="graph-flow-card-lead">${escapeHtml(subtitle)}</p>
507
+ <p class="graph-flow-card-body">${escapeHtml(graphBody)}</p>
508
+ </article>`);
509
+ const flow = cards
510
+ .flatMap((card, index) => (index < cards.length - 1 ? [card, '<span class="graph-flow-arrow" aria-hidden="true">→</span>'] : [card]))
511
+ .join('');
297
512
  return `<section class="graph-trinity">
298
- <div class="wide-heading">
299
- <p class="eyebrow">${escapeHtml(locale === 'ru' ? 'Модель продукта' : 'Product model')}</p>
300
- <h2>${escapeHtml(locale === 'ru' ? 'Три графа — один цикл разработки' : 'Three graphs, one development loop')}</h2>
301
- <p>${escapeHtml(locale === 'ru' ? 'Каждый граф — самостоятельный слой с чётким контрактом. Вместе они дают цикл, который агент может исполнять, а человек — аудировать.' : 'Each graph is a standalone layer with a clear contract. Together they form a loop an agent can execute and a human can audit.')}</p>
302
- </div>
303
- <div class="graph-flow">${graphs.map(([title, subtitle, body], index) => `<article>
304
- <span>${index + 1}</span>
305
- <h3>${escapeHtml(title)}</h3>
306
- <strong>${escapeHtml(subtitle)}</strong>
513
+ <div class="graph-trinity-heading">
514
+ <h2>${escapeHtml(title)}</h2>
307
515
  <p>${escapeHtml(body)}</p>
308
- </article>`).join('')}</div>
516
+ </div>
517
+ <div class="graph-flow">${flow}</div>
309
518
  </section>`;
310
519
  }
311
520
 
312
521
  function renderProductPillars(locale) {
313
- const pillars = locale === 'ru'
314
- ? [
315
- ['Атомы намерения', 'BVC: Basis · Vector · Goal минимальная единица смысла, которую понимает человек, git и агент.'],
316
- ['Контракт исполнения', 'Projection даёт input.targetFiles, output.evidenceRequired, verification.tier и матрицу разрешённых проверок.'],
317
- ['Гейты готовности', 'done без evidence = ошибка политики. assert_task_ready_for_done возвращает ok или violations[].'],
318
- ['Аудит-память', 'Память выводится из закрытых задач с валидными свидетельствами, а не из пересказа чата.'],
319
- ]
320
- : [
321
- ['Intent atoms', 'BVC: Basis · Vector · Goal is the smallest unit of meaning readable by humans, git and agents.'],
322
- ['Execution contract', 'Projection gives input.targetFiles, output.evidenceRequired, verification.tier and allowed checks.'],
323
- ['Readiness gates', 'done without evidence is a policy error. assert_task_ready_for_done returns ok or violations[].'],
324
- ['Audit memory', 'Memory is derived from closed tasks with valid evidence, not from a chat summary.'],
325
- ];
326
- return `<section class="pillar-section">
327
- <div class="wide-heading">
328
- <p class="eyebrow">${escapeHtml(locale === 'ru' ? 'Что внутри' : 'What is inside')}</p>
329
- <h2>${escapeHtml(locale === 'ru' ? 'Контрактный контур: намерение → исполнение → память' : 'Contract loop: intent → execution → memory')}</h2>
330
- </div>
331
- <div class="pillar-grid">${pillars.map(([title, body], index) => `<article>
332
- <span>${index + 1}</span>
333
- <h3>${escapeHtml(title)}</h3>
334
- <p>${escapeHtml(body)}</p>
335
- </article>`).join('')}</div>
336
- </section>`;
522
+ const items = locale === 'ru'
523
+ ? ['Аналитика', 'Задачи BVC', 'Доска задач', 'Проверки', 'Память проекта', 'MCP-инструменты']
524
+ : ['Analytics', 'BVC work items', 'Kanban board', 'Verification', 'Project memory', 'MCP tools'];
525
+ return renderIconLabelGrid({
526
+ title: locale === 'ru' ? 'Контрактный контур: намерение исполнение память' : 'Contract loop: intent → execution → memory',
527
+ body: locale === 'ru'
528
+ ? 'Свяжите стратегию с исполнением: от AN-разбора до проверенной памяти в git — в одном локальном графе работ.'
529
+ : 'Connect strategy to execution: from AN review to verified memory in git — in one local work graph.',
530
+ items,
531
+ });
337
532
  }
338
533
 
339
- function renderCodeShowcase(locale) {
534
+ function renderCodeShowcase(locale, options = {}) {
340
535
  const bvc = locale === 'ru'
341
- ? `#ImplementTraceLinksV1@ru<[\\nБазис:\\n Текущая трассировка шагов не валидируется в CI\\n Нет связи work.id ↔ файлы ↔ тесты\\nВектор:\\n Реализовать валидатор трассировки\\n Добавить MCP-инструмент get_unified_linkage\\nЦель:\\n Любая задача с trace.* метками имеет автоматическую проверку целостности\\n\\nМетки:\\n profile: work_item\\n tier: A\\n trace.codegen: false\\n\\nChecks:\\n npm run test:deterministic\\n bvc lint intent/**/implement-trace-links-v1.work.bvc\\n]>`
342
- : `#ImplementTraceLinksV1@en<[\\nBasis:\\n Current step tracing is not validated in CI\\n There is no work.id ↔ files ↔ tests linkage\\nVector:\\n Implement trace validator\\n Add MCP tool get_unified_linkage\\nGoal:\\n Any task with trace.* labels has automatic integrity checks\\n\\nLabels:\\n profile: work_item\\n tier: A\\n trace.codegen: false\\n\\nChecks:\\n npm run test:deterministic\\n bvc lint intent/**/implement-trace-links-v1.work.bvc\\n]>`;
343
- const mcp = `claim_work_item("implement-trace-links-v1")\\n→ get_work_contract(work_id)\\n→ edit target_files\\n→ run allowed commands\\n→ validate_evidence(structured_json)\\n→ assert_task_ready_for_done(work_id)\\n→ add_work_item_evidence + complete`;
536
+ ? `#ImplementTraceLinksV1@ru<[
537
+ Базис:
538
+ Текущая трассировка шагов не валидируется в CI
539
+ Нет связи work.id ↔ файлы ↔ тесты
540
+ Вектор:
541
+ Реализовать валидатор трассировки
542
+ Добавить MCP-инструмент get_unified_linkage
543
+ Цель:
544
+ Любая задача с trace.* метками имеет автоматическую проверку целостности
545
+
546
+ Метки:
547
+ profile: work_item
548
+ tier: A
549
+ trace.codegen: false
550
+
551
+ Checks:
552
+ npm run test:deterministic
553
+ bvc lint intent/**/implement-trace-links-v1.work.bvc
554
+ ]>`
555
+ : `#ImplementTraceLinksV1@en<[
556
+ Basis:
557
+ Current step tracing is not validated in CI
558
+ There is no work.id ↔ files ↔ tests linkage
559
+ Vector:
560
+ Implement trace validator
561
+ Add MCP tool get_unified_linkage
562
+ Goal:
563
+ Any task with trace.* labels has automatic integrity checks
564
+
565
+ Labels:
566
+ profile: work_item
567
+ tier: A
568
+ trace.codegen: false
569
+
570
+ Checks:
571
+ npm run test:deterministic
572
+ bvc lint intent/**/implement-trace-links-v1.work.bvc
573
+ ]>`;
574
+ const mcp = `claim_work_item("implement-trace-links-v1")
575
+ → get_work_contract(work_id)
576
+ → edit target_files
577
+ → run allowed commands
578
+ → validate_evidence(structured_json)
579
+ → assert_task_ready_for_done(work_id)
580
+ → add_work_item_evidence + complete`;
581
+ const title = options.title ?? (locale === 'ru' ? 'Задача читается человеком, git и агентом' : 'A task is readable by humans, git and agents');
582
+ const body = options.body ?? (locale === 'ru'
583
+ ? 'BVC описывает намерение, projection задаёт исполнение, evidence превращает результат в память.'
584
+ : 'BVC describes intent, projection defines execution and evidence turns the result into memory.');
344
585
  return `<section class="code-showcase">
345
586
  <div>
346
- <p class="eyebrow">${escapeHtml(locale === 'ru' ? 'Посмотрите на контракт' : 'Look at the contract')}</p>
347
- <h2>${escapeHtml(locale === 'ru' ? 'Задача читается человеком, git и агентом' : 'A task is readable by humans, git and agents')}</h2>
348
- <p>${escapeHtml(locale === 'ru' ? 'BVC описывает намерение, projection задаёт исполнение, evidence превращает результат в память.' : 'BVC describes intent, projection defines execution and evidence turns the result into memory.')}</p>
587
+ <h2>${escapeHtml(title)}</h2>
588
+ <p>${escapeHtml(body)}</p>
349
589
  </div>
350
590
  <div class="code-tabs">
351
- <div><strong>work.bvc</strong><pre><code>${escapeHtml(bvc)}</code></pre></div>
352
- <div><strong>MCP flow</strong><pre><code>${escapeHtml(mcp)}</code></pre></div>
591
+ <div><strong>work.bvc</strong><pre><code class="code-block language-bvc">${highlightBvcBlock(bvc)}</code></pre></div>
592
+ <div><strong>MCP flow</strong><pre><code class="code-block language-mcp">${highlightMcpFlow(mcp)}</code></pre></div>
353
593
  </div>
354
594
  </section>`;
355
595
  }
356
596
 
357
- function renderAudience(locale) {
358
- const groups = locale === 'ru'
359
- ? [
360
- ['Для техлида / архитектора', 'Канон смысла в .bvc, аудит по умолчанию, локальность в вашем git.'],
361
- ['Для разработчика', 'Один бэклог, меньше импровизации, понятный get_work_contract перед реализацией.'],
362
- ['Для агента', 'Явный input/output/verification, allowlist команд и structured evidence без фейка.'],
363
- ]
364
- : [
365
- ['For tech leads / architects', 'Meaning canon in .bvc, audit by default and local source of truth in your git.'],
366
- ['For developers', 'One backlog, less improvisation and clear get_work_contract before implementation.'],
367
- ['For agents', 'Explicit input/output/verification, command allowlist and structured evidence without fake done.'],
368
- ];
369
- return `<section class="audience-section">
370
- <h2>${escapeHtml(locale === 'ru' ? 'Для кого Work Graph' : 'Who Work Graph is for')}</h2>
371
- <div>${groups.map(([title, body]) => `<article><h3>${escapeHtml(title)}</h3><p>${escapeHtml(body)}</p></article>`).join('')}</div>
372
- </section>`;
597
+ function renderAudience(locale, copy) {
598
+ const ru = locale === 'ru';
599
+ return renderSiteSectionsBlock(
600
+ {
601
+ title: ru ? 'Для кого Work Graph' : 'Who Work Graph is for',
602
+ sections: ru
603
+ ? [
604
+ { title: 'Для техлида и архитектора', body: 'Видите связь решений, задач и evidence в одном графе: AN-разборы, BVC-контракты и проверки остаются в git, а не в пересказе чата.', icon: 'eye' },
605
+ { title: 'Для разработчика', body: 'Один бэклог и понятный get_work_contract перед кодом: меньше импровизации, ясные targetFiles и статусы на доске.', icon: 'users-three' },
606
+ { title: 'Для агента', body: 'Явные input/output/verification, allowlist команд и structured evidence: агент исполняет контракт, а не объявляет «готово» словами.', icon: 'chart-line-up' },
607
+ ]
608
+ : [
609
+ { title: 'For tech leads and architects', body: 'See decisions, work items and evidence in one graph: AN reviews, BVC contracts and checks stay in git, not in chat summaries.', icon: 'eye' },
610
+ { title: 'For developers', body: 'One backlog and a clear get_work_contract before coding: less improvisation, explicit targetFiles and board states.', icon: 'users-three' },
611
+ { title: 'For agents', body: 'Explicit input/output/verification, command allowlists and structured evidence: the agent executes the contract instead of saying done in prose.', icon: 'chart-line-up' },
612
+ ],
613
+ },
614
+ copy,
615
+ locale,
616
+ );
617
+ }
618
+
619
+ function renderInstallCopyIcon() {
620
+ return renderInlineIcon('copy-bold.svg', { className: 'install-copy-icon', size: 18 });
621
+ }
622
+
623
+ function renderInstallCodeBlock(command, locale) {
624
+ const copyLabel = locale === 'ru' ? 'Копировать' : 'Copy';
625
+ const copiedLabel = locale === 'ru' ? 'Скопировано' : 'Copied';
626
+ return `<div class="install-code">
627
+ <code>${escapeHtml(command)}</code>
628
+ <button type="button" class="install-copy-btn" data-copy-text="${escapeAttr(command)}" data-copy-label="${escapeAttr(copyLabel)}" data-copied-label="${escapeAttr(copiedLabel)}" aria-label="${escapeAttr(copyLabel)}" title="${escapeAttr(copyLabel)}">${renderInstallCopyIcon()}<span class="install-copy-btn-text">${escapeHtml(copyLabel)}</span></button>
629
+ </div>`;
373
630
  }
374
631
 
375
632
  function renderInstallInstructions(locale) {
376
- const steps = locale === 'ru'
377
- ? [
378
- ['Инициализируйте MCP', 'npx @work-graph/mcp init'],
379
- ['Добавьте в mcp.json', '"command": "npx @work-graph/mcp", "args": ["--workspace", "."]'],
380
- ['Проверьте подключение', 'Покажи список задач в статусе ready'],
381
- ['Соберите статический сайт', 'npm run build:public-site'],
382
- ]
383
- : [
384
- ['Initialize MCP', 'npx @work-graph/mcp init'],
385
- ['Add to mcp.json', '"command": "npx @work-graph/mcp", "args": ["--workspace", "."]'],
386
- ['Check connection', 'Show ready work items'],
387
- ['Build the static site', 'npm run build:public-site'],
388
- ];
633
+ const copy = locale === 'ru'
634
+ ? {
635
+ title: 'Как установить Work Graph',
636
+ lead: 'Установка в существующий репозиторий: локальный бэклог, UI и MCP для агента. Нужны Node.js 20+ и npm.',
637
+ projectDir: 'В каталоге проекта выполните:',
638
+ script: 'cd /path/to/your-project\nnpx @work-graph/cli init .\nnpm install\nnpm run workgraph:ui',
639
+ openUi: 'Откройте в браузере:',
640
+ uiUrl: 'http://127.0.0.1:4177/',
641
+ agentLead: 'Для агентов:',
642
+ agentQuote: 'Установи Work Graph в этот проект https://www.npmjs.com/package/@work-graph/cli и открой локальный UI.',
643
+ detail: 'Команда init создаёт .work-graph/config.json, intent/, npm-скрипты и при необходимости .cursor/mcp.json (npx -y @work-graph/mcp, WORKGRAPH_ROOT). Существующие intent/index.bvc и architecture/main.bvc сохраняются. После npm install перезагрузите MCP в IDE.',
644
+ verify: 'Проверка: npm run workgraph:doctor',
645
+ guideLabel: 'Подробная инструкция',
646
+ guideHref: 'https://github.com/bvc-lang/work-graph/blob/main/docs/getting-started.md',
647
+ }
648
+ : {
649
+ title: 'How to install Work Graph',
650
+ lead: 'Install into an existing repository: local backlog, operator UI, and MCP for your agent. Requires Node.js 20+ and npm.',
651
+ projectDir: 'In your project directory, run:',
652
+ script: 'cd /path/to/your-project\nnpx @work-graph/cli init .\nnpm install\nnpm run workgraph:ui',
653
+ openUi: 'Then open:',
654
+ uiUrl: 'http://127.0.0.1:4177/',
655
+ agentLead: 'For agents:',
656
+ agentQuote: 'Install Work Graph in this project https://www.npmjs.com/package/@work-graph/cli and open the local UI.',
657
+ detail: 'init writes .work-graph/config.json, intent/, npm scripts, and optional IDE files (for example .cursor/mcp.json with npx -y @work-graph/mcp and WORKGRAPH_ROOT). Existing intent/index.bvc and architecture/main.bvc are preserved. Reload MCP in your IDE after npm install.',
658
+ verify: 'Verify: npm run workgraph:doctor',
659
+ guideLabel: 'Detailed install guide',
660
+ guideHref: 'https://github.com/bvc-lang/work-graph/blob/main/docs/getting-started.md',
661
+ };
389
662
  return `<section id="install" class="install-section">
390
- <div>
391
- <p class="eyebrow">${escapeHtml(locale === 'ru' ? 'Установка' : 'Installation')}</p>
392
- <h2>${escapeHtml(locale === 'ru' ? 'Как установить Work Graph' : 'How to install Work Graph')}</h2>
393
- <p>${escapeHtml(locale === 'ru' ? 'Быстрый путь — подключить Work Graph как MCP-сервер к Cursor или Claude Code. Данные остаются локально в git, сайт собирается в dist/public-site без базы данных.' : 'The fastest path is to connect Work Graph as an MCP server to Cursor or Claude Code. Data stays local in git, and the site exports to dist/public-site without a database.')}</p>
663
+ <h2>${escapeHtml(copy.title)}</h2>
664
+ <p class="install-lead">${escapeHtml(copy.lead)}</p>
665
+ <div class="install-block">
666
+ <p class="install-block-label">${escapeHtml(copy.agentLead)}</p>
667
+ ${renderInstallCodeBlock(copy.agentQuote, locale)}
394
668
  </div>
395
- <ol>${steps.map(([title, command]) => `<li>
396
- <strong>${escapeHtml(title)}</strong>
397
- <code>${escapeHtml(command)}</code>
398
- </li>`).join('')}</ol>
669
+ <div class="install-block">
670
+ <p class="install-block-label">${escapeHtml(copy.projectDir)}</p>
671
+ ${renderInstallCodeBlock(copy.script, locale)}
672
+ </div>
673
+ <div class="install-block">
674
+ <p class="install-block-label">${escapeHtml(copy.openUi)}</p>
675
+ ${renderInstallCodeBlock(copy.uiUrl, locale)}
676
+ </div>
677
+ <p class="install-detail">${escapeHtml(copy.detail)}</p>
678
+ <p class="install-detail">${escapeHtml(copy.verify)}</p>
679
+ <p class="install-guide"><a class="install-guide-link" href="${escapeAttr(copy.guideHref)}" target="_blank" rel="noopener noreferrer">${escapeHtml(copy.guideLabel)} →</a></p>
399
680
  </section>`;
400
681
  }
401
682
 
402
683
  function renderComparisonStrip(locale) {
403
684
  const rows = locale === 'ru'
404
685
  ? [
405
- ['Обычный AI-воркфлоу', 'намерение в голове или чате', 'готово = слова агента'],
406
- ['Таск-трекер', 'планирует работу и статусы', 'не хранит машинный контракт evidence'],
407
- ['CI / тесты', 'проверяет команды', 'не знает зачем была задача'],
408
- ['Work Graph', 'связывает намерение, исполнение и память', 'готово = evidence + verified gate'],
686
+ { iconName: 'robot', name: 'Обычный AI-воркфлоу', does: 'намерение в голове или чате', gap: 'готово = слова агента' },
687
+ { iconName: 'kanban', name: 'Jira / Linear', does: 'планирует работу и статусы в облаке', gap: 'контракт и evidence не в git-репозитории' },
688
+ { iconName: 'test-tube', name: 'CI / тесты', does: 'проверяет команды', gap: 'не знает зачем была задача' },
689
+ { iconName: 'graph', name: 'Work Graph', does: 'связывает намерение, исполнение и память', gap: 'готово = evidence + verified gate' },
409
690
  ]
410
691
  : [
411
- ['Plain AI workflow', 'intent lives in heads or chats', 'done = agent words'],
412
- ['Task tracker', 'plans work and statuses', 'does not store machine evidence contract'],
413
- ['CI / tests', 'checks commands', 'does not know why the task exists'],
414
- ['Work Graph', 'links intent, execution and memory', 'done = evidence + verified gate'],
692
+ { iconName: 'robot', name: 'Plain AI workflow', does: 'intent lives in heads or chats', gap: 'done = agent words' },
693
+ { iconName: 'kanban', name: 'Jira / Linear', does: 'plans work and statuses in the cloud', gap: 'contract and evidence live outside the repo' },
694
+ { iconName: 'test-tube', name: 'CI / tests', does: 'checks commands', gap: 'does not know why the task exists' },
695
+ { iconName: 'graph', name: 'Work Graph', does: 'links intent, execution and memory', gap: 'done = evidence + verified gate' },
415
696
  ];
416
697
  return `<section class="comparison-strip">
417
- <h2>${escapeHtml(locale === 'ru' ? 'Ключевое отличие от обычного AI-воркфлоу' : 'What changes compared to a plain AI workflow')}</h2>
418
- <div>${rows.map(([name, does, gap]) => `<article>
419
- <h3>${escapeHtml(name)}</h3>
420
- <p>${escapeHtml(does)}</p>
421
- <strong>${escapeHtml(gap)}</strong>
698
+ <h2 class="comparison-strip-heading">${escapeHtml(locale === 'ru' ? 'Ключевое отличие от обычного AI-воркфлоу' : 'What changes compared to a plain AI workflow')}</h2>
699
+ <div class="comparison-strip-grid">${rows.map(({ iconName, name, does, gap }) => `<article class="comparison-strip-card">
700
+ <span class="comparison-strip-icon" aria-hidden="true">${comparisonStripIcon(iconName)}</span>
701
+ <h3 class="comparison-strip-card-title">${escapeHtml(name)}</h3>
702
+ <p class="comparison-strip-card-lead">${escapeHtml(does)}</p>
703
+ <p class="comparison-strip-card-gap">${escapeHtml(gap)}</p>
422
704
  </article>`).join('')}</div>
423
705
  </section>`;
424
706
  }
425
707
 
426
- function renderRoadmapFaq(locale) {
427
- const roadmap = locale === 'ru'
428
- ? ['BVC-задачи и доска', 'MCP-инструменты для агентов', 'Evidence для изменений в репозитории', 'Ready-for-done gate', 'Статический сайт и llms.txt', 'Дальше: шаблоны проектов, доменные профили, публичные демо-кейсы']
429
- : ['BVC work items and board', 'MCP tools for agents', 'Repository-change evidence', 'Ready-for-done gate', 'Static site and llms.txt', 'Next: project templates, domain profiles, public demo cases'];
430
- const faq = locale === 'ru'
431
- ? [
432
- ['Это замена Jira?', 'Нет. Jira планирует. Work Graph хранит локальный контракт работы, evidence и проверенную память рядом с кодом.'],
433
- ['Это IDE или агент?', 'Нет. Cursor и Claude Code исполняют. Work Graph задаёт контракт и проверяет готовность результата.'],
434
- ['Нужна база данных?', 'Для публичного сайта нет. Static export собирается в отдельную папку и хостится как обычные файлы.'],
435
- ]
436
- : [
437
- ['Is this a Jira replacement?', 'Work Graph is a contract platform around AI work: it can take intent from trackers, but the source of truth is the BVC contract and evidence in git.'],
438
- ['Is this an IDE or agent?', 'Cursor and Claude Code execute work. Work Graph links intent, execution and memory, then verifies readiness.'],
439
- ['Does it need a database?', 'Not for the public site. Static export builds a folder that can be hosted as plain files.'],
440
- ];
441
- return `<section class="roadmap-faq">
442
- <div>
443
- <h2>${escapeHtml(locale === 'ru' ? 'Roadmap' : 'Roadmap')}</h2>
444
- <ul>${roadmap.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
445
- </div>
446
- <div>
447
- <h2>${escapeHtml(locale === 'ru' ? 'Вопросы и ответы' : 'FAQ')}</h2>
448
- ${faq.map(([question, answer]) => `<details><summary>${escapeHtml(question)}</summary><p>${escapeHtml(answer)}</p></details>`).join('')}
708
+ function renderFaqToggleIcon() {
709
+ return '<span class="faq-toggle" aria-hidden="true"></span>';
710
+ }
711
+
712
+ function renderHomeFaq(locale) {
713
+ const items = getLocalizedFaq(locale).flatMap((category) => category.items);
714
+ return `<section id="faq" class="home-faq" aria-labelledby="home-faq-title">
715
+ <h2 id="home-faq-title" class="home-faq-title">FAQ</h2>
716
+ <div class="faq-accordion">
717
+ ${items.map((item) => `<details class="faq-item">
718
+ <summary><span class="faq-question">${escapeHtml(item.question)}</span>${renderFaqToggleIcon()}</summary>
719
+ <div class="faq-answer"><p>${escapeHtml(item.answer)}</p></div>
720
+ </details>`).join('')}
449
721
  </div>
722
+ <p class="faq-json-note">${escapeHtml(locale === 'ru' ? 'Для LLM и интеграций: ' : 'For LLMs and integrations: ')}<a href="/faq.json">/faq.json</a></p>
450
723
  </section>`;
451
724
  }
452
725
 
453
- function renderFaqPage(locale) {
454
- const faq = getLocalizedFaq(locale);
455
- return `<section class="faq-page" aria-labelledby="faq-title">
726
+ function renderPageSectionsGrid(sections, copy) {
727
+ return `<div class="content-layout page-sections">
728
+ <div class="article-column">
729
+ <div class="section-grid">${sections.map((section) => renderSection(section, copy)).join('')}</div>
730
+ </div>
731
+ </div>`;
732
+ }
733
+
734
+ function renderSiteSectionsBlock(block, copy, locale) {
735
+ const sections = resolveSiteSections(block.sections ?? [], locale);
736
+ const heading = block.title
737
+ ? `<div class="wide-heading site-sections-block__heading"><h2>${escapeHtml(block.title)}</h2>${block.intro ? `<p>${escapeHtml(block.intro)}</p>` : ''}</div>`
738
+ : '';
739
+ return `${heading}${renderPageSectionsGrid(sections, copy)}`;
740
+ }
741
+
742
+ function featureColumnsToSiteSections(block) {
743
+ return {
744
+ title: block.title,
745
+ intro: block.intro,
746
+ sections: (block.items ?? []).map(({ iconName, heading, body, badges }) => ({
747
+ title: heading,
748
+ body,
749
+ icon: iconName,
750
+ badges,
751
+ })),
752
+ };
753
+ }
754
+
755
+ function renderPageLead(block) {
756
+ return `<section class="page-lead wide-heading"><p>${escapeHtml(block.text)}</p></section>`;
757
+ }
758
+
759
+ function renderWorkflowPipeline(block) {
760
+ return `<section class="workflow-pipeline">
456
761
  <div class="wide-heading">
457
- <p class="eyebrow">${escapeHtml(locale === 'ru' ? 'FAQ' : 'FAQ')}</p>
458
- <h2 id="faq-title">${escapeHtml(locale === 'ru' ? 'Вопрос-ответ (FAQ) — Work Graph' : 'FAQ — Work Graph')}</h2>
459
- <p>${escapeHtml(locale === 'ru' ? 'Раздел структурирован для быстрого поиска человеком и для точного парсинга LLM-агентами.' : 'Structured for fast human search and precise LLM-agent parsing.')}</p>
762
+ <h2>${escapeHtml(block.title)}</h2>
763
+ ${block.intro ? `<p>${escapeHtml(block.intro)}</p>` : ''}
460
764
  </div>
461
- ${faq.map((category) => `<section class="faq-category">
462
- <h3>${escapeHtml(category.category)}</h3>
463
- ${category.items.map((item) => `<details>
464
- <summary>${escapeHtml(item.question)}</summary>
465
- <p>${escapeHtml(item.answer)}</p>
466
- </details>`).join('')}
467
- </section>`).join('')}
468
- <section class="faq-json-note">
469
- <h3>${escapeHtml(locale === 'ru' ? 'Для разработчиков и LLM' : 'For developers and LLMs')}</h3>
470
- <p>${escapeHtml(locale === 'ru' ? 'Структурированная версия доступна как Schema.org FAQPage по адресу /faq.json.' : 'A structured Schema.org FAQPage version is available at /faq.json.')}</p>
471
- <a href="/faq.json">/faq.json</a>
472
- </section>
765
+ <ol class="workflow-pipeline-steps">${block.steps.map(({ label, detail, iconName }) => `<li>
766
+ ${iconName ? `<span class="workflow-pipeline-icon" aria-hidden="true">${workflowPipelineIcon(iconName)}</span>` : ''}
767
+ <span class="workflow-pipeline-label">${escapeHtml(label)}</span>
768
+ <span class="workflow-pipeline-detail">${escapeHtml(detail)}</span>
769
+ </li>`).join('')}</ol>
473
770
  </section>`;
474
771
  }
475
772
 
476
- function renderHomeExpansion(locale) {
477
- return `${renderProofStats(locale)}
773
+ function renderCompareBoundaries(block) {
774
+ const muted = block.variant === 'muted';
775
+ return `<section class="compare-boundaries${muted ? ' compare-boundaries--muted' : ''}">
776
+ <h2 class="compare-boundaries-heading">${escapeHtml(block.title)}</h2>
777
+ <div class="compare-boundaries-grid">${block.items.map(({ iconName, heading, body }) => `<article>
778
+ <span class="compare-boundaries-icon" aria-hidden="true">${featureIcon(iconName)}</span>
779
+ <h3>${escapeHtml(heading)}</h3>
780
+ <p>${escapeHtml(body)}</p>
781
+ </article>`).join('')}</div>
782
+ </section>`;
783
+ }
784
+
785
+ function resolveSiteSections(sections, locale) {
786
+ return sections.map((section) => {
787
+ if (section.competitors) {
788
+ const { competitors: _flag, ...rest } = section;
789
+ return { ...rest, competitors: getPublicSiteCompetitors(locale) };
790
+ }
791
+ return section;
792
+ });
793
+ }
794
+
795
+ function renderPageBlock(block, locale, copy) {
796
+ switch (block.type) {
797
+ case 'lead':
798
+ return renderPageLead(block);
799
+ case 'pipeline':
800
+ return renderWorkflowPipeline(block);
801
+ case 'featureColumns':
802
+ return renderSiteSectionsBlock(featureColumnsToSiteSections(block), copy, locale);
803
+ case 'iconLabelGrid':
804
+ return renderIconLabelGrid(block);
805
+ case 'screenshotGallery':
806
+ return renderScreenshotGallery(locale, block);
807
+ case 'graphTrinity':
808
+ return renderGraphTrinity(locale, block);
809
+ case 'codeShowcase':
810
+ return renderCodeShowcase(locale, block);
811
+ case 'comparisonStrip':
812
+ return renderComparisonStrip(locale);
813
+ case 'siteSections':
814
+ return renderSiteSectionsBlock(block, copy, locale);
815
+ case 'boundaries':
816
+ return renderCompareBoundaries(block);
817
+ default:
818
+ return '';
819
+ }
820
+ }
821
+
822
+ function renderPageBlocks(page, locale, copy) {
823
+ const blocks = page.blocks ?? [];
824
+ if (blocks.length === 0 && page.sections?.length) {
825
+ return `<div class="page-flow">${renderPageSectionsGrid(page.sections, copy)}</div>`;
826
+ }
827
+ return `<div class="page-flow">${blocks.map((block) => renderPageBlock(block, locale, copy)).join('')}</div>`;
828
+ }
829
+
830
+ function renderHomePageSections(locale, copy, page) {
831
+ const introSections = page.sections.map((section) => renderSection(section, copy)).join('');
832
+ return `${renderSteps(copy, locale)}
833
+ ${renderInstallInstructions(locale)}
834
+ ${renderScreenshotGallery(locale)}
478
835
  ${renderGraphTrinity(locale)}
479
836
  ${renderProductPillars(locale)}
480
- ${renderInstallInstructions(locale)}
837
+ <div class="content-layout home-pillars">
838
+ <div class="article-column">
839
+ <div class="section-grid">${introSections}</div>
840
+ </div>
841
+ </div>
481
842
  ${renderCodeShowcase(locale)}
482
- ${renderAudience(locale)}
483
843
  ${renderComparisonStrip(locale)}
484
- ${renderRoadmapFaq(locale)}`;
844
+ ${renderAudience(locale, copy)}
845
+ ${renderHomeFaq(locale)}`;
485
846
  }
486
847
 
487
848
  export function renderPublicSiteHtml(page, options = {}) {
@@ -490,8 +851,9 @@ export function renderPublicSiteHtml(page, options = {}) {
490
851
  page.locale = locale;
491
852
  const copy = getPublicSiteCopy(locale);
492
853
  const jsonLd = JSON.stringify(publicSiteJsonLd(page));
854
+ const installHref = page.kind === 'home' ? '#install' : `${withLocalePath('/', locale)}#install`;
493
855
  const primaryButton = renderUiButton({
494
- href: '#install',
856
+ href: installHref,
495
857
  label: copy.hero.primary,
496
858
  variant: 'primary',
497
859
  size: 'lg',
@@ -502,37 +864,19 @@ export function renderPublicSiteHtml(page, options = {}) {
502
864
  variant: 'secondary',
503
865
  size: 'lg',
504
866
  });
867
+ const documentTitle = page.documentTitle
868
+ ?? (/^Work Graph\b/u.test(page.title) ? page.title : `${page.title} · Work Graph`);
505
869
  return `<!doctype html>
506
870
  <html lang="${locale}" data-theme="${theme}">
507
871
  <head>
508
872
  <meta charset="utf-8">
509
873
  <meta name="viewport" content="width=device-width, initial-scale=1">
510
- <title>${escapeHtml(page.title)} · Work Graph</title>
874
+ <title>${escapeHtml(documentTitle)}</title>
511
875
  <meta name="description" content="${escapeAttr(page.description)}">
512
876
  <link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
513
877
  <link rel="stylesheet" href="/assets/fonts/GraphikLCG/stylesheet.css">
514
878
  <link rel="stylesheet" href="/assets/design-tokens-workgraph-dark.css">
515
- <script>
516
- (function () {
517
- var params = new URLSearchParams(window.location.search);
518
- var allowedLang = ['en', 'ru'];
519
- var allowedTheme = ['light', 'dark'];
520
- var lang = params.get('lang') || localStorage.getItem('workGraphPublicSiteLocale') || '${locale}';
521
- var theme = params.get('theme') || localStorage.getItem('workGraphPublicSiteTheme') || '${theme}';
522
- if (!allowedLang.includes(lang)) lang = 'en';
523
- if (!allowedTheme.includes(theme)) theme = 'light';
524
- localStorage.setItem('workGraphPublicSiteLocale', lang);
525
- localStorage.setItem('workGraphPublicSiteTheme', theme);
526
- document.documentElement.lang = lang;
527
- document.documentElement.dataset.theme = theme;
528
- if (!params.get('lang') || !params.get('theme')) {
529
- var next = new URL(window.location.href);
530
- next.searchParams.set('lang', lang);
531
- next.searchParams.set('theme', theme);
532
- window.history.replaceState(null, '', next);
533
- }
534
- })();
535
- </script>
879
+ <script>${renderPublicSiteBootstrapScript(locale, theme)}</script>
536
880
  <script type="application/ld+json">${jsonLd}</script>
537
881
  <style>
538
882
  :root {
@@ -558,11 +902,17 @@ export function renderPublicSiteHtml(page, options = {}) {
558
902
  --ui-control-bg-rgb: 244 245 247;
559
903
  --ui-control-bg-hover-rgb: 235 236 240;
560
904
  --ui-radius-control: 3px;
905
+ --code-surface: #f4f5f7;
906
+ --code-header: #ebecf0;
907
+ --code-text: #172b4d;
908
+ --site-section-spacing: clamp(96px, 10vw, 128px);
909
+ --footer-heading-color: #172b4d;
910
+ --footer-link-color: #44546f;
561
911
  }
562
912
  html[data-theme="dark"] {
563
913
  color-scheme: dark;
564
914
  --bg: #1d2125;
565
- --header-bg: #161a1d;
915
+ --header-bg: #1d2125;
566
916
  --card: #282e33;
567
917
  --card-muted: #22272b;
568
918
  --text: #d6dde5;
@@ -571,157 +921,332 @@ export function renderPublicSiteHtml(page, options = {}) {
571
921
  --accent: #85b8ff;
572
922
  --accent-soft: #092957;
573
923
  --shadow: 0 1px 1px rgba(0, 0, 0, .32);
924
+ --code-surface: #22272b;
925
+ --code-header: #2c333a;
926
+ --code-text: #dfe1e6;
927
+ --footer-heading-color: #d6dde5;
928
+ --footer-link-color: #9fadbc;
574
929
  }
575
930
  * { box-sizing: border-box; }
576
931
  body { margin: 0; background: var(--bg); color: var(--text); font-family: var(--brand-font-sans, 'Graphik LCG', ui-sans-serif, system-ui, sans-serif); line-height: 1.55; }
577
932
  a { color: var(--accent); }
578
- .site-header, .site-footer { border-color: var(--border); padding: 16px clamp(18px, 5vw, 72px); }
579
- .site-header { align-items: center; background: color-mix(in srgb, var(--header-bg) 96%, transparent); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); display: flex; gap: 22px; justify-content: space-between; position: sticky; top: 0; z-index: 10; }
580
- .site-brand { color: var(--text); font-size: 15px; font-weight: 800; text-decoration: none; }
581
- .site-nav { display: flex; flex-wrap: wrap; gap: 4px; }
582
- .site-nav a, .locale-link { border-radius: 3px; color: var(--text); font-size: 14px; font-weight: 600; padding: 8px 10px; text-decoration: none; }
583
- .site-nav a:hover, .locale-link:hover, .locale-link.is-active { background: #ebecf0; }
933
+ .site-header { border-color: var(--border); padding: 16px clamp(18px, 5vw, 72px); }
934
+ .site-header { align-items: center; background: color-mix(in srgb, var(--header-bg) 96%, transparent); backdrop-filter: blur(14px); border-bottom: none; display: flex; flex-wrap: nowrap; gap: 22px; justify-content: space-between; position: sticky; top: 0; z-index: 10; }
935
+ html[data-theme="dark"] .site-header { background: var(--bg); backdrop-filter: none; }
936
+ .site-brand { align-items: center; display: inline-flex; flex: none; line-height: 0; text-decoration: none; }
937
+ .site-brand-logo { display: block; height: 24px; max-width: min(188px, 42vw); width: auto; }
938
+ .site-brand-emblem { display: none; height: 24px; width: auto; }
939
+ html[data-theme="dark"] .site-brand-logo { filter: brightness(0) invert(1); }
940
+ .site-nav { display: flex; flex: 1; flex-wrap: nowrap; gap: 2px; justify-content: center; min-width: 0; }
941
+ .site-nav a { border-radius: 3px; color: var(--text); font-size: 1.0625rem; font-weight: 500; padding: 8px 12px; text-decoration: none; transition: color .15s ease; }
942
+ .site-nav a:hover { background: transparent; color: var(--accent); }
943
+ .site-nav a[aria-current="page"] { color: var(--accent); }
944
+ .site-header-actions { align-items: center; display: flex; flex: none; gap: 8px; }
945
+ .site-control-btn.site-nav-toggle { display: none; padding: 0; width: 36px; }
946
+ .site-nav-toggle-bars { background: currentColor; border-radius: 1px; display: block; height: 2px; position: relative; width: 18px; }
947
+ .site-nav-toggle-bars::before, .site-nav-toggle-bars::after { background: currentColor; border-radius: 1px; content: ''; height: 2px; left: 0; position: absolute; width: 18px; }
948
+ .site-nav-toggle-bars::before { top: -6px; transition: transform .2s ease, top .2s ease; }
949
+ .site-nav-toggle-bars::after { top: 6px; transition: transform .2s ease, top .2s ease; }
950
+ .site-nav-toggle.is-open .site-nav-toggle-bars { background: transparent; }
951
+ .site-nav-toggle.is-open .site-nav-toggle-bars::before { top: 0; transform: rotate(45deg); }
952
+ .site-nav-toggle.is-open .site-nav-toggle-bars::after { top: 0; transform: rotate(-45deg); }
584
953
  .site-controls { align-items: center; display: flex; gap: 8px; }
954
+ .site-control-btn { align-items: center; background: var(--card-muted); border: 1px solid var(--border); border-radius: 999px; color: var(--text); cursor: pointer; display: inline-flex; font: inherit; font-size: 13px; font-weight: 700; height: 36px; justify-content: center; line-height: 1; padding: 0; }
955
+ .site-control-btn:hover { background: var(--card); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
956
+ .site-control-btn.theme-toggle { width: 36px; }
957
+ .site-control-btn.locale-toggle { letter-spacing: .03em; min-width: 42px; padding: 0 10px; }
958
+ .site-github-btn { flex: none; text-decoration: none; width: 36px; }
959
+ .site-github-icon-svg { display: block; }
960
+ .site-github-icon-svg path { fill: currentColor; }
961
+ .theme-toggle-icons { align-items: center; display: inline-flex; height: 18px; justify-content: center; position: relative; width: 18px; }
962
+ .theme-toggle-icons .theme-icon { inset: 0; position: absolute; }
963
+ html[data-theme="light"] .theme-icon-sun { display: none; }
964
+ html[data-theme="dark"] .theme-icon-moon { display: none; }
585
965
  .site-icon, .header-theme-toggle-icon { fill: currentColor; flex: none; vertical-align: -0.15em; }
586
- .site-main { padding: 0; }
587
- .site-shell { margin: 0 auto; max-width: 1360px; padding: clamp(34px, 6vw, 78px) clamp(18px, 5vw, 72px); }
588
- .hero { margin: 0 auto 30px; max-width: 980px; text-align: left; }
589
- .eyebrow { color: var(--accent); font-size: 12px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
966
+ .site-icon path, .header-theme-toggle-icon path { fill: currentColor; }
967
+ html { overflow-x: clip; }
968
+ .site-main { overflow-x: clip; padding: 0; }
969
+ .site-shell { --site-band-inner-max: 1280px; --site-shell-inline-pad: clamp(18px, 5vw, 72px); margin: 0 auto; max-width: 1360px; padding: clamp(34px, 6vw, 78px) var(--site-shell-inline-pad); }
970
+ .site-section-band { box-sizing: border-box; margin-inline: calc(50% - 50vw); max-width: 100vw; padding-block: clamp(48px, 6vw, 80px); padding-inline: max(var(--site-shell-inline-pad), calc((100vw - var(--site-band-inner-max)) / 2)); width: 100vw; }
971
+ .site-section-band--muted { background: var(--card-muted); }
972
+ .site-section-band--plain { background: var(--card); }
973
+ .site-section-band--surface { background: var(--bg); }
974
+ .site-shell > .site-section-band { margin-inline: calc(-1 * var(--site-shell-inline-pad)); max-width: none; padding-inline: var(--site-shell-inline-pad); width: auto; }
975
+ .site-section-band__inner { margin-inline: auto; max-width: var(--site-band-inner-max); min-width: 0; width: 100%; }
976
+ .hero { margin: 0 auto 48px; max-width: 980px; text-align: center; }
977
+ .site-shell--home .hero { margin-bottom: clamp(28px, 4vw, 40px); }
978
+ .site-shell--page .hero { margin-inline: 0; margin-bottom: clamp(32px, 4vw, 48px); max-width: 720px; text-align: left; }
979
+ .site-shell--page .hero p { margin-inline: 0; max-width: 58ch; }
980
+ .site-shell--page .hero .cta-row { justify-content: flex-start; }
981
+ .site-shell--page .page-flow { margin-inline: 0; }
982
+ .site-shell--page .page-flow.page-flow--narrow { margin-inline: 0; max-width: 720px; }
983
+ .site-shell--page .content-layout { justify-content: start; margin-inline: 0; max-width: 100%; }
984
+ .site-shell--page .section-heading { align-items: flex-start; }
985
+ .site-shell--page .site-section { text-align: left; }
986
+ .site-shell--page .doc-list { list-style: none; margin: 20px 0 0; padding: 0; }
987
+ .site-shell--page .doc-list li { margin: 0 0 22px; }
988
+ .site-shell--page .doc-list li:last-child { margin-bottom: 0; }
989
+ .site-shell--page .doc-list a { font-size: 1.0625rem; font-weight: 600; }
990
+ .site-shell--page .doc-list p { margin: 6px 0 0; max-width: 58ch; }
991
+ .home-hero-preview-wrap { margin: 0 auto var(--site-section-spacing); max-width: 1040px; width: 100%; }
992
+ .home-hero-preview-wrap .template-visual { margin: 0; }
993
+ .home-flow, .page-flow { display: flex; flex-direction: column; gap: var(--site-section-spacing); margin: 0 auto; max-width: 1200px; width: 100%; }
994
+ .page-flow .content-layout, .page-flow.page-flow--narrow .content-layout { margin-bottom: 0; max-width: 100%; }
995
+ .page-flow.page-flow--narrow { max-width: 820px; }
996
+ .home-flow > .graph-trinity, .home-flow > .icon-label-grid-section, .home-flow > .install-section, .home-flow > .site-section-band, .home-flow > .code-showcase, .home-flow > .comparison-strip, .home-flow > .home-faq, .home-flow > .steps-section, .page-flow > .page-lead, .page-flow > .workflow-pipeline, .page-flow > .graph-trinity, .page-flow > .icon-label-grid-section, .page-flow > .site-section-band, .page-flow > .code-showcase, .page-flow > .comparison-strip, .page-flow > .compare-boundaries, .page-flow > .content-layout, .page-flow > .site-sections-block__heading { margin-top: 0; }
997
+ .home-flow > .content-layout { margin-bottom: 0; max-width: 1200px; }
998
+ .home-flow .content-layout.home-pillars { justify-content: start; margin-inline: 0; max-width: 1200px; width: 100%; }
999
+ .home-flow .home-pillars .section-grid { gap: clamp(32px, 4vw, 56px) clamp(28px, 4vw, 48px); grid-template-columns: repeat(2, minmax(0, 1fr)); }
1000
+ .home-flow .home-pillars .site-section { border-bottom: 0; display: flex; flex-direction: column; padding: 0; text-align: left; }
1001
+ .home-flow .home-pillars .section-heading { align-items: flex-start; display: block; }
1002
+ .home-flow .home-pillars .section-heading h2 { font-size: clamp(1.375rem, 2.1vw, 1.75rem); font-weight: 800; letter-spacing: -.02em; line-height: 1.25; margin: 0 0 12px; }
1003
+ .home-flow .home-pillars .site-section p { flex: 1 1 auto; font-size: 1rem; line-height: 1.65; margin: 0; }
1004
+ .home-flow .home-pillars .badge-row { margin: 16px 0 0; }
1005
+ .site-sections-block__heading { margin-bottom: clamp(20px, 3vw, 32px); }
1006
+ .site-sections-block__heading p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; max-width: 58ch; }
590
1007
  h1 { font-size: clamp(2.25rem, 4.8vw, 4rem); letter-spacing: -.035em; line-height: 1.04; margin: 12px 0 18px; }
591
1008
  h2 { font-size: clamp(1.35rem, 2.2vw, 1.9rem); letter-spacing: -.015em; line-height: 1.18; margin: 0 0 10px; }
592
- .hero p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; max-width: 820px; }
1009
+ .hero p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin-left: auto; margin-right: auto; max-width: 820px; }
1010
+ .hero .cta-row { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 24px; }
593
1011
  .cta-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
594
- .template-visual { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow-raised); margin: 0 auto 56px; max-width: 1040px; overflow: hidden; }
1012
+ .template-visual { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow-raised); margin: 0 auto; max-width: 1040px; overflow: hidden; }
595
1013
  .screenshot-hero img { display: block; height: auto; width: 100%; }
596
1014
  .screenshot-hero figcaption { border-top: 1px solid var(--border); color: var(--muted); font-size: 13px; padding: 12px 16px; }
597
- .visual-toolbar { align-items: center; background: #fafbfc; border-bottom: 1px solid var(--border); display: flex; gap: 8px; padding: 12px 14px; }
598
- .visual-toolbar span { background: var(--border); border-radius: 999px; height: 8px; width: 8px; }
599
- .visual-board { background: #f4f5f7; display: grid; gap: 12px; grid-template-columns: repeat(4, 1fr); min-height: 310px; padding: 18px; }
600
- .visual-column { background: #ebecf0; border-radius: 3px; padding: 10px; }
601
- .visual-column h3 { color: var(--muted); font-size: 11px; font-weight: 800; letter-spacing: .04em; margin: 0 0 10px; text-transform: uppercase; }
602
- .visual-card { background: var(--card); border-left: 3px solid var(--accent); border-radius: 3px; box-shadow: var(--shadow); display: grid; gap: 7px; margin-bottom: 10px; min-height: 66px; padding: 10px; }
603
- .visual-card.is-1 { border-left-color: #36b37e; }
604
- .visual-card.is-2 { border-left-color: #ffab00; }
605
- .visual-card.is-3 { border-left-color: #6554c0; }
606
- .visual-card span, .visual-card p, .visual-card small, .related-preview span { background: var(--border); border-radius: 999px; display: block; height: 6px; }
607
- .visual-card p { width: 80%; }
608
- .visual-card small { width: 44%; }
609
- .content-layout { align-items: start; display: grid; gap: 56px; grid-template-columns: 300px minmax(0, 820px); justify-content: center; }
610
- .template-aside { position: sticky; top: 92px; }
611
- .aside-card { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow); padding: 22px; }
612
- .aside-icon { align-items: center; background: var(--accent-soft); border-radius: 4px; color: var(--accent); display: flex; height: 42px; justify-content: center; margin-bottom: 14px; width: 42px; }
613
- .aside-card h2 { font-size: 22px; line-height: 1.18; }
614
- .aside-card p { color: var(--muted); font-size: 14px; }
615
- .aside-card .wg-btn { justify-content: center; width: 100%; }
616
- .aside-card dl { display: grid; gap: 0; margin: 20px 0 0; }
617
- .aside-card dl div { align-items: center; border-top: 1px solid var(--border); display: flex; justify-content: space-between; padding: 11px 0 0; margin-top: 11px; }
618
- .aside-card dt { color: var(--muted); font-size: 12px; }
619
- .aside-card dd { margin: 0; }
620
- .article-column { min-width: 0; }
621
- .section-grid { display: grid; gap: 36px; grid-template-columns: 1fr; margin-top: 0; }
622
- .site-section { background: transparent; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; box-shadow: none; padding: 0 0 34px; }
1015
+ .content-layout { display: grid; justify-content: center; margin-bottom: 24px; max-width: 820px; margin-left: auto; margin-right: auto; width: 100%; }
1016
+ .article-column { min-width: 0; width: 100%; }
1017
+ .section-grid { display: grid; gap: 48px; grid-template-columns: 1fr; margin-top: 0; }
1018
+ .site-section { background: transparent; border: 0; border-bottom: 1px solid var(--border); border-radius: 0; box-shadow: none; padding: 0 0 44px; }
623
1019
  .section-heading { align-items: center; display: flex; gap: 10px; }
624
- .section-icon { align-items: center; background: var(--accent-soft); border-radius: 4px; color: var(--accent); display: inline-flex; height: 34px; justify-content: center; width: 34px; }
1020
+ .section-icon { align-items: center; background: var(--accent-soft); border-radius: 4px; box-sizing: border-box; color: var(--accent); display: inline-flex; flex: none; height: 34px; justify-content: center; width: 52px; }
625
1021
  .site-section p, .doc-list p { color: var(--muted); font-size: 16px; line-height: 1.7; }
626
1022
  .badge-row { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
627
1023
  .flow-list { display: grid; gap: 8px; padding-left: 22px; }
628
- table { border-collapse: collapse; width: 100%; }
629
- th, td { border-top: 1px solid var(--border); padding: 8px; text-align: left; vertical-align: top; }
630
- .proof-stats { display: grid; gap: 14px; grid-template-columns: repeat(4, 1fr); margin: 56px auto 10px; max-width: 1200px; }
631
- .proof-stats div { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: none; padding: 20px; }
632
- .proof-stats strong { color: var(--accent); display: block; font-size: clamp(1.8rem, 4vw, 3rem); letter-spacing: -.04em; line-height: 1; }
633
- .proof-stats span { color: var(--muted); display: block; font-size: 13px; margin-top: 8px; }
634
- .graph-trinity, .pillar-section, .install-section, .code-showcase, .audience-section, .comparison-strip, .roadmap-faq { margin: 58px auto 0; max-width: 1200px; }
635
- .screenshot-gallery { margin: 58px auto 0; max-width: 1200px; }
636
- .screenshot-grid { display: grid; gap: 18px; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 18px; }
637
- .screenshot-card { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow); margin: 0; overflow: hidden; }
638
- .screenshot-card img { display: block; height: auto; width: 100%; }
639
- .screenshot-card figcaption { border-top: 1px solid var(--border); display: grid; gap: 4px; padding: 14px; }
640
- .screenshot-card figcaption span { color: var(--muted); font-size: 13px; }
1024
+ .table-scroll { -webkit-overflow-scrolling: touch; margin-top: 16px; max-width: 100%; overflow-x: auto; }
1025
+ .table-scroll table { border-collapse: collapse; min-width: 720px; width: 100%; }
1026
+ .table-scroll th, .table-scroll td { border-top: 1px solid var(--border); padding: 8px 10px; text-align: left; vertical-align: top; white-space: normal; word-break: break-word; }
1027
+ .article-column, .site-section { max-width: 100%; min-width: 0; }
1028
+ .graph-trinity, .icon-label-grid-section, .install-section, .code-showcase, .feature-columns-section, .comparison-strip, .home-faq { margin: var(--site-section-spacing) auto 0; max-width: 1200px; width: 100%; }
1029
+ .icon-label-grid-section { background: transparent; box-sizing: border-box; padding: 0; text-align: left; }
1030
+ .icon-label-grid-heading { margin: 0; max-width: 720px; }
1031
+ .icon-label-grid-heading h2 { font-size: clamp(1.5rem, 2.8vw, 2.125rem); letter-spacing: -.02em; margin: 0 0 16px; }
1032
+ .icon-label-grid-heading p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; max-width: 58ch; }
1033
+ .icon-label-grid { column-gap: clamp(24px, 4vw, 48px); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: clamp(44px, 6vw, 64px); row-gap: clamp(48px, 6vw, 72px); }
1034
+ .icon-label-grid-item { align-items: center; display: flex; flex-direction: row; gap: 16px; justify-content: flex-start; text-align: left; }
1035
+ .icon-label-grid-icon { display: block; flex: none; line-height: 0; }
1036
+ .icon-label-grid-icon-box { align-items: center; background: var(--accent); border-radius: 6px; color: #fff; display: inline-flex; height: 40px; justify-content: center; width: 56px; }
1037
+ .icon-label-grid-icon-svg { color: #fff; display: block; flex: none; }
1038
+ .icon-label-grid-icon-svg polyline { stroke: currentColor; }
1039
+ .icon-label-grid-label { color: var(--text); font-size: 1.375rem; font-weight: 700; letter-spacing: -.015em; line-height: 1.35; max-width: none; }
1040
+ .feature-columns-section { text-align: center; }
1041
+ .feature-columns-heading { font-size: clamp(1.35rem, 2.2vw, 1.9rem); letter-spacing: -.015em; margin: 0 0 clamp(32px, 5vw, 48px); }
1042
+ .feature-columns { display: grid; gap: clamp(28px, 4vw, 56px); grid-template-columns: repeat(3, minmax(0, 1fr)); }
1043
+ .feature-column { align-items: center; display: flex; flex-direction: column; text-align: center; }
1044
+ .feature-column-icon { align-items: center; color: var(--accent); display: inline-flex; height: 56px; justify-content: center; margin-bottom: 20px; width: 56px; }
1045
+ .feature-column-icon-svg { display: block; flex: none; }
1046
+ .feature-column-icon-svg path { fill: currentColor; }
1047
+ .feature-column h3 { font-size: 1.375rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.3; margin: 0 auto 12px; max-width: 24ch; }
1048
+ .feature-column p { color: var(--muted); font-size: 1rem; line-height: 1.65; margin: 0 auto; max-width: 36ch; }
1049
+ .screenshot-gallery { background: transparent; margin: 0; max-width: 100%; min-width: 0; overflow: hidden; padding: 0; width: 100%; }
1050
+ .screenshot-gallery-heading, .screenshot-switcher { margin-left: auto; margin-right: auto; max-width: 100%; min-width: 0; }
1051
+ .screenshot-gallery-heading { text-align: center; }
1052
+ .screenshot-gallery-heading h2 { color: var(--text); font-size: clamp(1.85rem, 3.4vw, 2.5rem); font-weight: 800; letter-spacing: -.03em; margin: 10px 0 18px; }
1053
+ .screenshot-gallery-lead { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0 auto; max-width: 58ch; }
1054
+ .screenshot-switcher { margin-top: clamp(36px, 4vw, 52px); }
1055
+ .screenshot-tablist { display: flex; flex-wrap: wrap; gap: 8px 20px; justify-content: center; margin: 0 0 clamp(40px, 5vw, 56px); padding: 0; }
1056
+ .screenshot-tab { background: transparent; border: 0; border-radius: 999px; color: color-mix(in srgb, var(--text) 72%, transparent); cursor: pointer; font: inherit; font-size: 1.2rem; font-weight: 400; line-height: 1.2; padding: 8px 16px; transition: background .15s ease, color .15s ease; }
1057
+ .screenshot-tab:hover { color: var(--accent); }
1058
+ .screenshot-tab.is-active { background: var(--accent); color: #fff; }
1059
+ html[data-theme="dark"] .screenshot-tab.is-active { color: #fff; }
1060
+ .screenshot-panels { min-height: 320px; min-width: 0; position: relative; }
1061
+ .screenshot-panel { align-items: start; display: none; gap: clamp(24px, 3vw, 40px); grid-template-columns: minmax(0, 0.4fr) minmax(0, 0.6fr); min-width: 0; }
1062
+ .screenshot-panel.is-active { display: grid; }
1063
+ .screenshot-panel-copy { grid-column: 1; max-width: 28rem; padding-top: 16px; text-align: left; }
1064
+ .screenshot-panel-copy h3 { color: var(--text); font-size: clamp(1.625rem, 2.5vw, 2.25rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.2; margin: 0 0 20px; }
1065
+ .screenshot-panel-copy p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; }
1066
+ .screenshot-panel-visual { grid-column: 2; min-width: 0; width: 100%; }
1067
+ .screenshot-panel-frame { background: var(--card); border-radius: 12px; box-shadow: 0 12px 36px rgba(9, 30, 66, .12), 0 0 1px rgba(9, 30, 66, .16); overflow: hidden; }
1068
+ .screenshot-panel-frame img { border: 0; border-radius: 0; box-shadow: none; display: block; height: auto; width: 100%; }
641
1069
  .wide-heading { max-width: 900px; }
642
- .graph-flow { display: grid; gap: 18px; grid-template-columns: repeat(3, minmax(0, 1fr)); margin-top: 20px; }
643
- .graph-flow article { background: var(--card); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); padding: 22px; position: relative; }
644
- .graph-flow article + article::before { color: var(--accent); content: '→'; font-size: 28px; font-weight: 800; left: -26px; position: absolute; top: 42px; }
645
- .graph-flow span { align-items: center; background: var(--accent-soft); border-radius: 999px; color: var(--accent); display: inline-flex; font-weight: 800; height: 30px; justify-content: center; width: 30px; }
646
- .graph-flow strong { color: var(--text); display: block; margin-bottom: 8px; }
647
- .graph-flow p, .graph-trinity .wide-heading p { color: var(--muted); }
648
- .pillar-grid { display: grid; gap: 16px; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 18px; }
649
- .pillar-grid article, .comparison-strip article, .roadmap-faq > div { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: none; padding: 22px; }
650
- .pillar-grid article span { align-items: center; background: var(--accent-soft); border-radius: 999px; color: var(--accent); display: inline-flex; font-weight: 800; height: 28px; justify-content: center; width: 28px; }
651
- .pillar-grid h3, .comparison-strip h3 { margin-bottom: 6px; }
652
- .pillar-grid p, .comparison-strip p, .roadmap-faq p, .code-showcase p { color: var(--muted); }
653
- .install-section { align-items: start; background: var(--card-muted); border: 1px solid var(--border); border-radius: 4px; display: grid; gap: 28px; grid-template-columns: .9fr 1.1fr; padding: 28px; }
654
- .install-section p { color: var(--muted); }
655
- .install-section ol { counter-reset: install; display: grid; gap: 12px; list-style: none; margin: 0; padding: 0; }
656
- .install-section li { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: none; display: grid; gap: 8px; padding: 14px; }
657
- .install-section li::before { align-items: center; background: var(--accent-soft); border-radius: 999px; color: var(--accent); content: counter(install); counter-increment: install; display: inline-flex; font-weight: 800; height: 24px; justify-content: center; width: 24px; }
658
- .install-section code { background: #101214; border-radius: 6px; color: #dfe1e6; display: block; padding: 10px; }
1070
+ .page-lead p { color: var(--muted); font-size: 1.125rem; line-height: 1.7; margin: 0; max-width: 58ch; }
1071
+ .workflow-pipeline-steps { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); list-style: none; margin: 24px 0 0; padding: 0; }
1072
+ .workflow-pipeline-steps li { background: var(--card); border: 1px solid var(--border); border-radius: 8px; display: flex; flex-direction: column; gap: 8px; padding: 20px 18px; }
1073
+ .workflow-pipeline-icon { align-items: flex-start; color: var(--text); display: inline-flex; flex: none; margin-bottom: 4px; }
1074
+ .workflow-pipeline-icon-svg { display: block; flex: none; }
1075
+ .workflow-pipeline-icon-svg path { fill: currentColor; }
1076
+ .workflow-pipeline-label { color: var(--text); font-size: 0.9375rem; font-weight: 700; line-height: 1.35; }
1077
+ .workflow-pipeline-detail { color: var(--muted); font-size: 0.875rem; line-height: 1.5; }
1078
+ .compare-boundaries { margin-top: 0; }
1079
+ .compare-boundaries-heading { font-size: clamp(1.35rem, 2.4vw, 1.75rem); font-weight: 800; letter-spacing: -.02em; margin: 0 0 24px; }
1080
+ .compare-boundaries-grid { display: grid; gap: 20px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
1081
+ .compare-boundaries-grid article { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 22px; }
1082
+ .compare-boundaries--muted .compare-boundaries-grid article { background: var(--card-muted); }
1083
+ .compare-boundaries-icon { color: var(--text); display: inline-flex; margin-bottom: 14px; }
1084
+ .compare-boundaries-grid h3 { font-size: 1.125rem; font-weight: 700; margin: 0 0 8px; }
1085
+ .compare-boundaries-grid p { color: var(--muted); font-size: 0.9375rem; line-height: 1.6; margin: 0; }
1086
+ .graph-trinity { text-align: left; }
1087
+ .graph-trinity-heading { margin: 0 0 clamp(24px, 3vw, 36px); max-width: 58ch; }
1088
+ .graph-trinity-heading h2 { font-size: clamp(1.35rem, 2.2vw, 1.9rem); letter-spacing: -.015em; line-height: 1.2; margin: 0 0 12px; }
1089
+ .graph-trinity-heading p { color: var(--muted); font-size: 1.0625rem; line-height: 1.65; margin: 0; }
1090
+ .graph-flow { align-items: stretch; display: grid; gap: clamp(10px, 1.5vw, 16px); grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto minmax(0, 1fr); margin: 0; }
1091
+ .graph-flow-card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow); box-sizing: border-box; display: flex; flex-direction: column; min-height: 100%; padding: clamp(20px, 2.5vw, 24px); }
1092
+ .graph-flow-step { align-items: center; background: var(--accent-soft); border-radius: 999px; color: var(--accent); display: inline-flex; flex: none; font-size: 0.875rem; font-weight: 800; height: 32px; justify-content: center; line-height: 1; margin: 0 0 14px; width: 32px; }
1093
+ .graph-flow-card-title { color: var(--text); font-size: 1.125rem; font-weight: 700; letter-spacing: -.01em; line-height: 1.3; margin: 0 0 6px; }
1094
+ .graph-flow-card-lead { color: var(--text); flex: none; font-size: 1rem; font-weight: 700; line-height: 1.35; margin: 0 0 10px; }
1095
+ .graph-flow-card-body { color: var(--muted); flex: 1 1 auto; font-size: 0.9375rem; line-height: 1.6; margin: 0; }
1096
+ .graph-flow-arrow { align-self: center; color: var(--accent); display: flex; flex: none; font-size: 1.5rem; font-weight: 800; justify-content: center; line-height: 1; padding: 0 2px; user-select: none; }
1097
+ .comparison-strip-heading { font-size: clamp(1.75rem, 3vw, 2.25rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.2; margin: 0 0 clamp(28px, 4vw, 40px); }
1098
+ .comparison-strip-grid { display: grid; gap: 20px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
1099
+ .comparison-strip-card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; box-shadow: none; display: flex; flex-direction: column; min-height: clamp(260px, 24vw, 320px); padding: 32px 28px 36px; }
1100
+ .comparison-strip-icon { align-items: flex-start; color: var(--text); display: inline-flex; flex: none; margin: 0 0 24px; min-height: 40px; }
1101
+ .comparison-strip-icon-svg { display: block; flex: none; }
1102
+ .comparison-strip-icon-svg path { fill: currentColor; }
1103
+ .comparison-strip-card-title { color: var(--text); font-size: 1.375rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.3; margin: 0 0 12px; }
1104
+ .comparison-strip-card-lead { color: var(--muted); flex: 1 1 auto; font-size: 0.9375rem; line-height: 1.55; margin: 0; }
1105
+ .comparison-strip-card-gap { color: var(--text); flex: none; font-size: 0.9375rem; font-weight: 700; line-height: 1.45; margin: auto 0 0; padding-top: 16px; }
1106
+ .home-faq .wide-heading p, .code-showcase p { color: var(--muted); }
1107
+ .install-section { background: var(--card-muted); border: 1px solid var(--border); border-radius: 8px; box-sizing: border-box; display: grid; gap: 0; grid-template-columns: 1fr; margin-inline: auto; max-width: min(100%, 720px); padding: clamp(24px, 4vw, 32px); width: 100%; }
1108
+ .install-section h2 { font-size: clamp(1.5rem, 2.8vw, 2rem); font-weight: 800; letter-spacing: -.02em; line-height: 1.15; margin: 0 0 14px; }
1109
+ .install-lead { color: var(--muted); font-size: 1rem; line-height: 1.65; margin: 0 0 22px; }
1110
+ .install-block { margin: 0 0 20px; }
1111
+ .install-block-label { color: var(--text); font-size: 0.9375rem; font-weight: 600; line-height: 1.4; margin: 0 0 8px; }
1112
+ .install-detail { color: var(--muted); font-size: 0.9375rem; line-height: 1.65; margin: 0 0 12px; }
1113
+ .install-guide { margin: 4px 0 0; }
1114
+ .install-guide-link { font-size: 0.9375rem; font-weight: 600; text-decoration: none; }
1115
+ .install-guide-link:hover { text-decoration: underline; }
1116
+ .install-code { position: relative; }
1117
+ .install-code code { background: var(--code-surface); border: 1px solid var(--border); border-radius: 8px; color: var(--code-text); display: block; font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; font-size: 15px; line-height: 1.45; padding: 14px 48px 14px 14px; white-space: pre-wrap; word-break: break-word; }
1118
+ .install-copy-btn { align-items: center; background: transparent; border: 0; border-radius: 6px; color: var(--muted); cursor: pointer; display: inline-flex; height: 32px; justify-content: center; padding: 0; position: absolute; right: 6px; top: 6px; width: 32px; }
1119
+ .install-copy-btn-text { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; white-space: nowrap; width: 1px; }
1120
+ .install-copy-icon { display: block; flex: none; }
1121
+ .install-copy-icon polyline, .install-copy-icon rect { stroke: currentColor; }
1122
+ .install-copy-btn:hover { background: var(--accent-soft); color: var(--accent); }
1123
+ .install-copy-btn.is-copied { background: var(--accent-soft); color: var(--accent); }
659
1124
  .code-showcase { align-items: start; display: grid; gap: 24px; grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr); }
660
1125
  .code-tabs { display: grid; gap: 14px; }
661
- .code-tabs > div { background: #101214; border-radius: 4px; color: #dfe1e6; overflow: hidden; }
662
- .code-tabs strong { background: #1f2428; color: #fff; display: block; padding: 10px 14px; }
663
- pre { margin: 0; overflow-x: auto; padding: 14px; white-space: pre-wrap; }
664
- code { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; font-size: 12px; }
665
- .comparison-strip > div { display: grid; gap: 14px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
666
- .comparison-strip strong { color: var(--text); display: block; font-size: 13px; }
667
- .audience-section > div { display: grid; gap: 16px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
668
- .audience-section article { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: none; padding: 22px; }
669
- .audience-section p { color: var(--muted); }
670
- .roadmap-faq { align-items: start; display: grid; gap: 22px; grid-template-columns: 1fr 1fr; }
671
- .faq-page { margin: 0 auto; max-width: 1000px; }
672
- .faq-category { margin-top: 34px; }
673
- .faq-category h3 { border-bottom: 1px solid var(--border); padding-bottom: 10px; }
674
- .faq-category details { background: var(--card); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow); margin: 10px 0; padding: 14px 16px; }
675
- .faq-json-note { background: var(--card-muted); border: 1px solid var(--border); border-radius: 10px; margin-top: 32px; padding: 18px; }
676
- .faq-json-note p { color: var(--muted); }
677
- .roadmap-faq ul { display: grid; gap: 8px; padding-left: 20px; }
678
- details { border-top: 1px solid var(--border); padding: 12px 0; }
679
- summary { cursor: pointer; font-weight: 700; }
680
- .steps-section { margin-top: 32px; }
681
- .jira-steps { counter-reset: steps; display: grid; gap: 18px; list-style: none; padding: 0; }
682
- .jira-steps li { display: grid; gap: 14px; grid-template-columns: 32px 1fr; }
683
- .step-number { align-items: center; background: #deebff; border-radius: 999px; color: #0747a6; display: inline-flex; font-weight: 700; height: 28px; justify-content: center; width: 28px; }
684
- .jira-steps h3 { font-size: 17px; margin: 0 0 4px; }
685
- .jira-steps p { color: var(--muted); margin: 0; }
686
- .related-templates { background: var(--card-muted); margin-top: 68px; padding: 54px 0; }
687
- .related-inner { margin: 0 auto; max-width: 1200px; padding: 0 24px; }
688
- .related-grid { display: grid; gap: 18px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
689
- .related-card { background: var(--card); border: 1px solid var(--border); border-radius: 4px; box-shadow: var(--shadow); padding: 18px; }
690
- .related-preview { background: var(--card-muted); border-radius: 3px; display: grid; gap: 8px; margin-bottom: 14px; padding: 18px; }
691
- .related-card h3 { margin: 0 0 8px; }
692
- .related-card p { color: var(--muted); }
693
- .bottom-cta { background: #101214; color: #fff; margin: 0 auto; max-width: 1200px; padding: 32px; text-align: center; }
694
- .bottom-cta h2 { color: #fff; }
695
- .site-footer { border-top: 1px solid var(--border); color: var(--muted); }
696
- .footer-columns { display: grid; gap: 20px; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0 auto; max-width: 1200px; }
697
- .footer-columns h3 { color: var(--text); font-size: 13px; }
698
- .footer-columns a { color: var(--muted); display: block; font-size: 13px; margin: 5px 0; text-decoration: none; }
1126
+ .code-tabs > div { background: var(--code-surface); border: 1px solid var(--border); border-radius: 4px; color: var(--code-text); overflow: hidden; }
1127
+ .code-tabs strong { background: var(--code-header); border-bottom: 1px solid var(--border); color: var(--code-text); display: block; font-size: 0.9375rem; padding: 12px 16px; }
1128
+ pre { background: transparent; color: inherit; margin: 0; overflow-x: auto; padding: 16px 18px; white-space: pre-wrap; }
1129
+ .code-tabs code { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', monospace; font-size: 0.875rem; line-height: 1.55; }
1130
+ .code-tabs .code-hl-key { color: #0d7a6f; }
1131
+ .code-tabs .code-hl-string { color: #7b4bb7; }
1132
+ .code-tabs .code-hl-keyword { color: #0052cc; }
1133
+ .code-tabs .code-hl-number { color: #de350b; }
1134
+ .code-tabs .code-hl-punct { color: #6b778c; }
1135
+ .code-tabs .code-hl-comment { color: #6b778c; }
1136
+ html[data-theme="dark"] .code-tabs .code-hl-key { color: #4ec9b0; }
1137
+ html[data-theme="dark"] .code-tabs .code-hl-string { color: #c792ea; }
1138
+ html[data-theme="dark"] .code-tabs .code-hl-keyword { color: #569cd6; }
1139
+ html[data-theme="dark"] .code-tabs .code-hl-number { color: #f78c6c; }
1140
+ html[data-theme="dark"] .code-tabs .code-hl-punct { color: #a8b3cf; }
1141
+ html[data-theme="dark"] .code-tabs .code-hl-comment { color: #8b9cb3; }
1142
+ .home-faq { margin-left: auto; margin-right: auto; max-width: 920px; padding-bottom: 12px; text-align: center; }
1143
+ .home-faq-title { font-size: clamp(2.35rem, 5vw, 3.25rem); font-weight: 800; letter-spacing: -.03em; margin: 0 0 40px; text-align: center; }
1144
+ .faq-accordion { border-top: 1px solid var(--border); text-align: left; }
1145
+ .faq-item { border-bottom: 1px solid var(--border); }
1146
+ .faq-item summary { align-items: center; cursor: pointer; display: flex; gap: 14px; justify-content: space-between; list-style: none; padding: 22px 0; }
1147
+ .faq-item summary::-webkit-details-marker { display: none; }
1148
+ .faq-question { color: var(--text); flex: 1; font-size: 1.3125rem; font-weight: 600; line-height: 1.45; min-width: 0; transition: color .15s ease; }
1149
+ .faq-item summary:hover .faq-question { color: var(--accent); }
1150
+ .faq-toggle { align-items: center; background: var(--accent); border-radius: 50%; color: #fff; display: inline-flex; flex: none; height: 32px; justify-content: center; padding: 0; width: 32px; }
1151
+ .faq-toggle::before { content: '+'; display: block; font-size: 22px; font-weight: 500; line-height: 1; }
1152
+ .faq-item[open] .faq-toggle::before { content: '−'; font-size: 24px; }
1153
+ .faq-answer { padding: 0 0 24px; }
1154
+ .faq-answer p { color: var(--muted); font-size: 1.0625rem; line-height: 1.7; margin: 0; max-width: 820px; }
1155
+ .home-faq .faq-json-note { color: var(--muted); font-size: 13px; margin-top: 28px; text-align: center; }
1156
+ .steps-section { box-sizing: border-box; margin-top: 0; max-width: 1200px; padding: clamp(8px, 2vw, 24px) 0 0; width: 100%; }
1157
+ .steps-section h2 { font-size: clamp(1.75rem, 3.2vw, 2.375rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.15; margin: 0 0 clamp(36px, 5vw, 56px); }
1158
+ .jira-steps { display: grid; gap: clamp(28px, 4vw, 40px) clamp(32px, 4vw, 56px); grid-template-columns: repeat(2, minmax(0, 1fr)); list-style: none; margin: 0; padding: 0; }
1159
+ .jira-steps li { align-items: start; display: grid; gap: 16px; grid-template-columns: 40px 1fr; }
1160
+ .step-number { align-items: center; color: var(--accent); display: inline-flex; flex: none; height: 32px; justify-content: center; margin-top: 2px; width: 32px; }
1161
+ .step-number-icon-svg { display: block; flex: none; }
1162
+ .step-number-icon-svg path { fill: currentColor; }
1163
+ .jira-steps h3 { color: var(--text); font-size: clamp(1.125rem, 2vw, 1.3125rem); font-weight: 400; line-height: 1.55; margin: 0; }
1164
+ .jira-steps h3 strong { font-weight: 700; }
1165
+ .bottom-cta { background: rgb(var(--ui-accent-rgb, 0 82 204)); box-sizing: border-box; color: #fff; margin: clamp(80px, 10vw, 120px) calc(50% - 50vw) 0; max-width: none; padding: clamp(64px, 8vw, 96px) max(clamp(18px, 5vw, 72px), calc((100vw - 1280px) / 2)); text-align: center; width: 100vw; }
1166
+ .bottom-cta__inner { margin-inline: auto; max-width: 900px; }
1167
+ .bottom-cta h2 { color: #fff; font-size: clamp(2rem, 4vw, 2.75rem); font-weight: 800; letter-spacing: -.03em; line-height: 1.15; margin: 0 0 clamp(24px, 3vw, 36px); }
1168
+ .bottom-cta .wg-btn { background: #fff; color: rgb(var(--ui-accent-rgb, 0 82 204)); }
1169
+ .bottom-cta .wg-btn:hover:not(:disabled) { background: #ebecf0; color: rgb(var(--ui-accent-hover-rgb, 0 101 255)); }
1170
+ .site-footer { background: var(--bg); color: var(--muted); margin-top: 0; padding: clamp(48px, 6vw, 72px) clamp(18px, 5vw, 72px) clamp(32px, 4vw, 48px); }
1171
+ .site-footer__panel { background: var(--card-muted); border-radius: 16px; margin: 0 auto; max-width: 1200px; padding: clamp(40px, 5vw, 56px) clamp(28px, 4vw, 48px); }
1172
+ .site-footer__grid { align-items: start; display: grid; gap: clamp(32px, 4vw, 48px); grid-template-columns: minmax(180px, 1.2fr) repeat(3, minmax(0, 1fr)); }
1173
+ .site-footer__brand { display: flex; flex-direction: column; gap: 24px; }
1174
+ .site-footer__logo { display: inline-block; line-height: 0; text-decoration: none; }
1175
+ .site-footer__logo img { display: block; height: 22px; max-width: min(168px, 100%); width: auto; }
1176
+ html[data-theme="dark"] .site-footer__logo img { filter: brightness(0) invert(1); }
1177
+ .site-footer__brand-links { display: flex; flex-direction: column; gap: 10px; }
1178
+ .site-footer__brand-links a, .footer-column a { color: var(--footer-link-color); display: block; font-size: 1rem; font-weight: 400; line-height: 1.5; text-decoration: none; }
1179
+ .site-footer__brand-links a:hover, .footer-column a:hover { color: var(--accent); text-decoration: none; }
1180
+ .footer-columns { display: contents; }
1181
+ .footer-column { display: flex; flex-direction: column; }
1182
+ .footer-column h3 { color: var(--footer-heading-color); font-size: 0.9375rem; font-weight: 700; letter-spacing: .05em; line-height: 1.35; margin: 0 0 20px; text-transform: uppercase; }
1183
+ .footer-column a { margin: 0 0 10px; }
1184
+ .footer-column a:last-child { margin-bottom: 0; }
1185
+ .site-footer__bottom { align-items: center; display: flex; flex-wrap: wrap; gap: 16px 28px; justify-content: space-between; margin: 28px auto 0; max-width: 1200px; }
1186
+ .site-footer__copy { color: var(--muted); font-size: 1rem; line-height: 1.5; margin: 0; }
1187
+ .site-footer__legal { align-items: center; display: flex; flex-wrap: wrap; gap: 8px 24px; }
1188
+ .site-footer__legal a { color: var(--footer-link-color); font-size: 1rem; font-weight: 400; text-decoration: none; }
1189
+ .site-footer__legal a:hover { color: var(--accent); }
699
1190
  ${UI_BUTTON_CSS}
1191
+ .site-main .wg-btn, .bottom-cta .wg-btn { border-radius: 999px; font-weight: 600; }
1192
+ .site-main .wg-btn--lg, .bottom-cta .wg-btn--lg { font-size: 1.0625rem; padding: 12px 22px; }
1193
+ .site-main .wg-btn--md { font-size: 0.9375rem; padding: 10px 18px; }
1194
+ .site-main .wg-btn--sm { font-size: 0.875rem; padding: 8px 16px; }
1195
+ .site-control-btn { font-size: 0.875rem; font-weight: 600; }
700
1196
  ${UI_BADGE_CSS}
701
- @media (max-width: 760px) {
702
- .site-header { align-items: flex-start; flex-direction: column; gap: 12px; position: static; }
703
- .site-nav { display: grid; gap: 6px; grid-template-columns: repeat(2, minmax(0, 1fr)); width: 100%; }
704
- .site-controls { justify-content: space-between; width: 100%; }
1197
+ @media (min-width: 1025px) and (max-width: 1280px) {
1198
+ .site-header { gap: 14px; }
1199
+ .site-nav a { font-size: 0.9375rem; padding: 8px 8px; }
1200
+ }
1201
+ @media (max-width: 1024px) {
1202
+ html.is-nav-scroll-locked { overflow: hidden; }
1203
+ .site-header { align-items: center; backdrop-filter: none; background: var(--bg); flex-wrap: nowrap; gap: 0 12px; }
1204
+ html[data-theme="dark"] .site-header { background: var(--bg); }
1205
+ .site-brand { flex: 1; min-width: 0; }
1206
+ .site-brand .site-brand-logo { display: none; }
1207
+ .site-brand .site-brand-emblem { display: block; height: 24px; }
1208
+ .site-header-actions { flex: none; margin-left: auto; }
1209
+ .site-control-btn.site-nav-toggle { display: inline-flex; }
1210
+ .site-nav { display: none; flex: none; min-width: 0; }
1211
+ .site-header.is-nav-open { align-content: start; background: var(--bg); box-sizing: border-box; display: grid; gap: 0 12px; grid-template-areas: "brand actions" "nav nav"; grid-template-columns: 1fr auto; grid-template-rows: auto minmax(0, 1fr); height: 100dvh; inset: 0; padding: calc(16px + env(safe-area-inset-top, 0px)) clamp(18px, 5vw, 32px) max(16px, env(safe-area-inset-bottom, 0px)); position: fixed; width: 100vw; z-index: 110; }
1212
+ .site-header.is-nav-open .site-brand { align-self: center; grid-area: brand; }
1213
+ .site-header.is-nav-open .site-header-actions { align-self: center; grid-area: actions; margin-left: 0; }
1214
+ .site-header.is-nav-open .site-nav { align-items: stretch; background: transparent; border-top: none; box-sizing: border-box; display: flex; flex-direction: column; gap: 0; grid-area: nav; height: auto; inset: auto; justify-content: flex-start; margin: 4px 0 0; max-height: none; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding: 0; position: relative; width: 100%; -webkit-overflow-scrolling: touch; }
1215
+ .site-header.is-nav-open .site-nav a { font-size: 1.25rem; padding: 10px 4px; white-space: normal; }
705
1216
  h1 { font-size: clamp(2rem, 12vw, 3.1rem); }
706
- .visual-board { grid-template-columns: repeat(2, 1fr); }
707
- .content-layout { grid-template-columns: 1fr; }
708
- .template-aside { position: static; }
709
- .proof-stats, .graph-flow, .pillar-grid, .install-section, .code-showcase, .audience-section > div, .comparison-strip > div, .roadmap-faq, .related-grid, .footer-columns, .screenshot-grid { grid-template-columns: 1fr; }
710
- .graph-flow article + article::before { content: none; }
711
- table { display: block; overflow-x: auto; white-space: nowrap; }
1217
+ .site-section-band { padding-inline: var(--site-shell-inline-pad); }
1218
+ .screenshot-tab { font-size: 1.125rem; padding: 7px 14px; }
1219
+ .screenshot-tablist { flex-wrap: nowrap; justify-content: flex-start; margin-bottom: 28px; -webkit-overflow-scrolling: touch; overflow-x: auto; padding-bottom: 6px; scrollbar-width: thin; }
1220
+ .screenshot-panel.is-active { grid-template-columns: 1fr; }
1221
+ .screenshot-panel-copy, .screenshot-panel-visual { grid-column: 1; }
1222
+ .screenshot-panel-copy { max-width: none; }
1223
+ .screenshot-panel-visual { margin-top: 8px; }
1224
+ .jira-steps { gap: 28px; grid-template-columns: 1fr; }
1225
+ .home-flow .home-pillars .section-grid { gap: 40px; grid-template-columns: 1fr; }
1226
+ .graph-flow { gap: 16px; grid-template-columns: 1fr; }
1227
+ .graph-flow-arrow { justify-self: center; padding: 4px 0; transform: rotate(90deg); }
1228
+ .icon-label-grid, .install-section, .code-showcase, .feature-columns, .comparison-strip-grid, .workflow-pipeline-steps, .compare-boundaries-grid { grid-template-columns: 1fr; }
1229
+ .site-footer__grid { grid-template-columns: 1fr; }
1230
+ .footer-columns { display: grid; gap: 32px; grid-template-columns: 1fr; }
1231
+ .icon-label-grid { column-gap: 20px; grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 48px; }
1232
+ .icon-label-grid-label { max-width: none; }
1233
+ .feature-column h3, .feature-column p { max-width: none; }
712
1234
  }
713
1235
  </style>
714
1236
  </head>
715
1237
  <body>
716
1238
  <header class="site-header">
717
- <a class="site-brand" href="/">Work Graph</a>
718
- ${renderNav(locale, theme)}
719
- ${renderThemeLocaleControls(locale, theme)}
1239
+ ${renderSiteBrand(locale)}
1240
+ ${renderNav(locale, page.route ?? '/')}
1241
+ <div class="site-header-actions">
1242
+ ${renderHeaderGithubButton(locale)}
1243
+ ${renderThemeLocaleControls(locale, theme)}
1244
+ ${renderNavToggle(locale)}
1245
+ </div>
720
1246
  </header>
721
1247
  <main class="site-main">
722
- <article class="site-shell">
1248
+ <article class="site-shell${page.kind === 'home' ? ' site-shell--home' : ' site-shell--page'}">
723
1249
  <header class="hero">
724
- <p class="eyebrow">${escapeHtml(copy.hero.eyebrow)}</p>
725
1250
  <h1>${escapeHtml(page.title)}</h1>
726
1251
  <p>${escapeHtml(page.description)}</p>
727
1252
  <div class="cta-row">
@@ -729,31 +1254,15 @@ export function renderPublicSiteHtml(page, options = {}) {
729
1254
  ${secondaryButton}
730
1255
  </div>
731
1256
  </header>
732
- ${page.kind === 'faq' ? renderFaqPage(locale) : `${renderHeroVisual(locale)}
733
- <div class="content-layout">
734
- ${renderTemplateAside(copy, locale, theme)}
735
- <div class="article-column">
736
- <div class="section-grid">${page.sections.map((section) => renderSection(section, copy)).join('')}</div>
737
- ${renderSteps(copy, locale)}
738
- </div>
739
- </div>
740
- ${page.kind === 'home' ? `${renderScreenshotGallery(locale)}${renderHomeExpansion(locale)}` : ''}`}
1257
+ ${page.kind === 'home' || page.kind === 'product' ? `<div class="home-hero-preview-wrap">${renderHeroVisual(locale, theme)}</div>` : ''}
1258
+ ${page.kind === 'home'
1259
+ ? `<div class="home-flow">${renderHomePageSections(locale, copy, page)}</div>`
1260
+ : renderPageBlocks(page, locale, copy)}
741
1261
  </article>
742
- ${renderRelatedTemplates(locale, theme)}
743
1262
  ${renderBottomCta(copy, locale, theme)}
744
1263
  </main>
745
- <footer class="site-footer">
746
- ${renderFooterColumns(locale)}
747
- <span>schema: ${PUBLIC_SITE_SCHEMA}</span>
748
- </footer>
749
- <script>
750
- document.addEventListener('click', function (event) {
751
- var link = event.target.closest('a[data-locale-value], a[data-theme-toggle]');
752
- if (!link) return;
753
- if (link.dataset.localeValue) localStorage.setItem('workGraphPublicSiteLocale', link.dataset.localeValue);
754
- if (link.dataset.themeValue) localStorage.setItem('workGraphPublicSiteTheme', link.dataset.themeValue);
755
- });
756
- </script>
1264
+ ${renderFooter(locale)}
1265
+ <script>${renderPublicSiteControlsScript()}</script>
757
1266
  </body>
758
1267
  </html>`;
759
1268
  }
@@ -774,40 +1283,43 @@ export function handlePublicSiteRequest(request, response, url) {
774
1283
  const method = request.method ?? 'GET';
775
1284
  if (method !== 'GET') return false;
776
1285
 
777
- const locale = normalizeLocale(url.searchParams.get('lang') ?? 'en');
1286
+ const locale = normalizeLocale(
1287
+ url.searchParams.get('lang') ?? localeFromPathname(url.pathname),
1288
+ );
778
1289
  const theme = normalizeTheme(url.searchParams.get('theme') ?? 'light');
1290
+ const routePathname = stripLocalePathPrefix(url.pathname);
779
1291
 
780
- if (url.pathname === '/llms.txt') {
1292
+ if (routePathname === '/llms.txt') {
781
1293
  sendText(response, 200, buildLlmsTxt(), 'text/plain');
782
1294
  return true;
783
1295
  }
784
1296
 
785
- if (url.pathname === '/.well-known/mcp.json') {
1297
+ if (routePathname === '/.well-known/mcp.json') {
786
1298
  sendJson(response, 200, buildMcpDiscovery());
787
1299
  return true;
788
1300
  }
789
1301
 
790
- if (url.pathname === '/faq.json') {
1302
+ if (routePathname === '/faq.json') {
791
1303
  sendJson(response, 200, buildFaqJsonLd(locale));
792
1304
  return true;
793
1305
  }
794
1306
 
795
- if (url.pathname === '/api/docs/bvc-authoring-context') {
1307
+ if (routePathname === '/api/docs/bvc-authoring-context') {
796
1308
  sendJson(response, 200, buildDocsContext('bvc-authoring'));
797
1309
  return true;
798
1310
  }
799
1311
 
800
- if (url.pathname === '/api/docs/mcp-tools-context') {
1312
+ if (routePathname === '/api/docs/mcp-tools-context') {
801
1313
  sendJson(response, 200, buildDocsContext('mcp-tools'));
802
1314
  return true;
803
1315
  }
804
1316
 
805
- if (url.pathname === '/api/docs/errors-context') {
1317
+ if (routePathname === '/api/docs/errors-context') {
806
1318
  sendJson(response, 200, buildDocsContext('errors'));
807
1319
  return true;
808
1320
  }
809
1321
 
810
- const markdownMatch = url.pathname.match(/^\/docs\/([^/.]+)\.md$/u);
1322
+ const markdownMatch = routePathname.match(/^\/docs\/([^/.]+)\.md$/u);
811
1323
  if (markdownMatch) {
812
1324
  const markdown = renderPublicDocMarkdown(markdownMatch[1], locale);
813
1325
  if (markdown == null) return false;
@@ -815,7 +1327,7 @@ export function handlePublicSiteRequest(request, response, url) {
815
1327
  return true;
816
1328
  }
817
1329
 
818
- const bvcExampleMatch = url.pathname.match(/^\/docs\/([^/.]+)\.bvc\.example$/u);
1330
+ const bvcExampleMatch = routePathname.match(/^\/docs\/([^/.]+)\.bvc\.example$/u);
819
1331
  if (bvcExampleMatch) {
820
1332
  const example = renderBvcExample(bvcExampleMatch[1]);
821
1333
  if (example == null) return false;
@@ -823,17 +1335,18 @@ export function handlePublicSiteRequest(request, response, url) {
823
1335
  return true;
824
1336
  }
825
1337
 
826
- if (url.pathname === '/docs.md') {
1338
+ if (routePathname === '/docs.md') {
1339
+ const docsPrefix = locale === 'en' ? '/en' : '';
827
1340
  const body = `# Work Graph Docs\n\n${PUBLIC_DOCS.map((doc) => {
828
1341
  const localized = getPublicSitePage(`/docs/${doc.slug}`, locale);
829
- return `- [${localized.title}](/docs/${doc.slug}.md?lang=${locale}): ${localized.description}`;
1342
+ return `- [${localized.title}](${docsPrefix}/docs/${doc.slug}.md): ${localized.description}`;
830
1343
  }).join('\n')}\n`;
831
1344
  sendText(response, 200, body, 'text/markdown');
832
1345
  return true;
833
1346
  }
834
1347
 
835
1348
  if (url.searchParams.get('format') === 'markdown') {
836
- const docSlug = url.pathname === '/docs' ? null : url.pathname.match(/^\/docs\/([^/.]+)$/u)?.[1];
1349
+ const docSlug = routePathname === '/docs' ? null : routePathname.match(/^\/docs\/([^/.]+)$/u)?.[1];
837
1350
  const markdown = docSlug
838
1351
  ? renderPublicDocMarkdown(docSlug, locale)
839
1352
  : `# Work Graph\n\n${buildLlmsTxt()}`;
@@ -842,9 +1355,9 @@ export function handlePublicSiteRequest(request, response, url) {
842
1355
  return true;
843
1356
  }
844
1357
 
845
- const page = getPublicSitePage(url.pathname, locale);
1358
+ const page = getPublicSitePage(routePathname, locale);
846
1359
  if (!page) return false;
847
- sendText(response, 200, renderPublicSiteHtml(page, { locale, theme }), 'text/html');
1360
+ sendText(response, 200, renderPublicSiteHtml(page, { locale, theme, route: routePathname }), 'text/html');
848
1361
  return true;
849
1362
  }
850
1363