From 18e25d473cfbfe560ab4095fd57a68da49043657 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 27 May 2026 15:28:03 +0200 Subject: [PATCH 1/9] Add a crawl check to make sure the online deployment works. --- scripts/crawl_check.mjs | 258 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 scripts/crawl_check.mjs diff --git a/scripts/crawl_check.mjs b/scripts/crawl_check.mjs new file mode 100644 index 00000000..ce3a0a23 --- /dev/null +++ b/scripts/crawl_check.mjs @@ -0,0 +1,258 @@ +// External-link crawler for a deployed site. Starts at a URL, +// recursively GETs every same-origin/same-basepath page, extracts +// links, and verifies each link responds 2xx (HEAD for cross-origin, +// GET for same-origin since we need the HTML anyway). +// +// Usage: +// node scripts/crawl_check.mjs [--concurrency N] [--timeout MS] +// node scripts/crawl_check.mjs --skip-external +// +// Exits 0 if all links are reachable, 1 if any are broken. + +import { Parser } from "htmlparser2"; + +const args = process.argv.slice(2); +let startArg = null; +let concurrency = 10; +let timeoutMs = 15000; +let skipExternal = false; +for (let i = 0; i < args.length; i++) { + if (args[i] === "--concurrency") concurrency = Number(args[++i]); + else if (args[i] === "--timeout") timeoutMs = Number(args[++i]); + else if (args[i] === "--skip-external") skipExternal = true; + else if (args[i].startsWith("-")) { console.error(`unknown flag: ${args[i]}`); process.exit(2); } + else if (!startArg) startArg = args[i]; +} +if (!startArg) { + console.error("usage: node scripts/crawl_check.mjs [--concurrency N] [--timeout MS] [--skip-external]"); + process.exit(2); +} + +const startUrl = new URL(startArg); +const origin = startUrl.origin; +const basePath = startUrl.pathname.endsWith("/") ? startUrl.pathname : startUrl.pathname + "/"; + +const SKIP_SCHEMES = /^(mailto:|tel:|javascript:|data:|ftp:)/i; +const FRAGMENT_TARGETS = new Map(); // url (no fragment) -> Set of ids found + +const crawlQueue = [startUrl.href]; +const crawled = new Set(); +const linkStatus = new Map(); // url -> { ok, status, error?, redirected? } +const linkSources = new Map(); // url -> Set of source pages +const linkFragments = new Map(); // url (no fragment) -> Set of [fragment, source] + +function shouldSkip(href) { + if (!href) return true; + if (href.startsWith("#")) return true; + if (SKIP_SCHEMES.test(href)) return true; + return false; +} + +function splitFragment(url) { + const i = url.indexOf("#"); + if (i < 0) return [url, null]; + return [url.slice(0, i), url.slice(i + 1)]; +} + +function isCrawlable(url) { + return url.origin === origin && url.pathname.startsWith(basePath); +} + +async function fetchWithTimeout(url, options) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + return await fetch(url, { ...options, signal: controller.signal, redirect: "follow" }); + } finally { + clearTimeout(timer); + } +} + +async function checkUrl(url) { + if (linkStatus.has(url)) return linkStatus.get(url); + let result; + try { + let res = await fetchWithTimeout(url, { method: "HEAD" }); + if (res.status === 405 || res.status === 501) { + res = await fetchWithTimeout(url, { method: "GET" }); + } + result = { + ok: res.ok, + status: res.status, + redirected: res.redirected ? res.url : null, + }; + } catch (e) { + result = { ok: false, status: 0, error: e.name === "AbortError" ? "timeout" : e.message }; + } + linkStatus.set(url, result); + return result; +} + +function extractFromHtml(html) { + const links = []; + const ids = new Set(); + const parser = new Parser({ + onopentag(name, attrs) { + if (attrs.id) ids.add(attrs.id); + if (attrs.name && (name === "a" || name === "input")) ids.add(attrs.name); + if (name === "a" && attrs.href) links.push(attrs.href); + else if (name === "link" && attrs.href) links.push(attrs.href); + else if (name === "img" && attrs.src) links.push(attrs.src); + else if (name === "script" && attrs.src) links.push(attrs.src); + else if (name === "iframe" && attrs.src) links.push(attrs.src); + }, + }); + parser.write(html); + parser.end(); + return { links, ids }; +} + +async function crawlOne(url) { + let res; + try { + res = await fetchWithTimeout(url, { method: "GET" }); + } catch (e) { + linkStatus.set(url, { ok: false, status: 0, error: e.name === "AbortError" ? "timeout" : e.message }); + return; + } + linkStatus.set(url, { + ok: res.ok, + status: res.status, + redirected: res.redirected ? res.url : null, + }); + if (!res.ok) return; + + const ct = res.headers.get("content-type") || ""; + if (!ct.includes("text/html") && !ct.includes("xhtml")) return; + + let html; + try { html = await res.text(); } catch { return; } + const { links, ids } = extractFromHtml(html); + + // Index ids on the final (redirected) URL so fragment links resolve. + FRAGMENT_TARGETS.set(res.url.split("#")[0], ids); + FRAGMENT_TARGETS.set(url, ids); + + for (const rawHref of links) { + if (shouldSkip(rawHref)) continue; + let abs; + try { abs = new URL(rawHref, res.url); } catch { continue; } + if (abs.protocol !== "http:" && abs.protocol !== "https:") continue; + + const [bare, frag] = splitFragment(abs.href); + + if (!linkSources.has(bare)) linkSources.set(bare, new Set()); + linkSources.get(bare).add(url); + + if (frag) { + if (!linkFragments.has(bare)) linkFragments.set(bare, []); + linkFragments.get(bare).push({ frag, source: url }); + } + + if (isCrawlable(abs)) { + if (!crawled.has(bare)) crawlQueue.push(bare); + } + } +} + +async function workerPool(items, fn, n) { + let i = 0; + const workers = []; + for (let k = 0; k < n; k++) { + workers.push((async () => { + while (i < items.length) { + const idx = i++; + await fn(items[idx]); + } + })()); + } + await Promise.all(workers); +} + +async function main() { + const t0 = Date.now(); + process.stderr.write(`Crawling ${startUrl.href}\n`); + process.stderr.write(` origin=${origin} basePath=${basePath} concurrency=${concurrency}\n\n`); + + // Phase 1: BFS crawl same-origin pages. + while (crawlQueue.length > 0) { + const batch = []; + while (crawlQueue.length > 0 && batch.length < concurrency * 4) { + const u = crawlQueue.shift(); + if (crawled.has(u)) continue; + crawled.add(u); + batch.push(u); + } + if (batch.length === 0) continue; + process.stderr.write(` [crawl] ${crawled.size} pages discovered, processing batch of ${batch.length}...\n`); + await workerPool(batch, crawlOne, concurrency); + } + + // Phase 2: check every external link that hasn't been visited. + const toCheck = []; + for (const url of linkSources.keys()) { + if (linkStatus.has(url)) continue; + if (skipExternal && new URL(url).origin !== origin) continue; + toCheck.push(url); + } + if (toCheck.length > 0) { + process.stderr.write(`\n [check] ${toCheck.length} external links to verify...\n`); + await workerPool(toCheck, checkUrl, concurrency); + } + + // Report. + const broken = []; + const fragmentMisses = []; + for (const [url, status] of linkStatus) { + if (!status.ok) { + const sources = [...(linkSources.get(url) || [])]; + broken.push({ url, status, sources }); + } + } + for (const [bare, frags] of linkFragments) { + const ids = FRAGMENT_TARGETS.get(bare); + if (!ids) continue; // not crawled (external) -- skip fragment check + const seen = new Set(); + for (const { frag, source } of frags) { + if (ids.has(frag)) continue; + const key = `${bare}#${frag}<-${source}`; + if (seen.has(key)) continue; + seen.add(key); + fragmentMisses.push({ url: bare, frag, source }); + } + } + + const elapsed = ((Date.now() - t0) / 1000).toFixed(1); + console.log(``); + console.log(`Crawl complete in ${elapsed}s:`); + console.log(` Pages crawled: ${crawled.size}`); + console.log(` Unique links: ${linkSources.size}`); + console.log(` Status checks: ${linkStatus.size}`); + console.log(` Broken links: ${broken.length}`); + console.log(` Missing anchors: ${fragmentMisses.length}`); + + if (broken.length > 0) { + broken.sort((a, b) => a.url.localeCompare(b.url)); + console.log(`\nBroken links:`); + for (const b of broken) { + const tag = b.status.error ? `ERR ${b.status.error}` : `${b.status.status}`; + console.log(` [${tag}] ${b.url}`); + const srcs = b.sources.slice().sort(); + for (const s of srcs.slice(0, 5)) console.log(` on: ${s}`); + if (srcs.length > 5) console.log(` ... and ${srcs.length - 5} more`); + } + } + + if (fragmentMisses.length > 0) { + fragmentMisses.sort((a, b) => (a.url + a.frag).localeCompare(b.url + b.frag)); + console.log(`\nMissing fragments:`); + for (const f of fragmentMisses) { + console.log(` ${f.url}#${f.frag}`); + console.log(` on: ${f.source}`); + } + } + + process.exit((broken.length > 0 || fragmentMisses.length > 0) ? 1 : 0); +} + +main().catch((e) => { console.error(e); process.exit(2); }); From eaa33cd61d7d0618276078a1ebdc6c4a9b3c9210 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 27 May 2026 15:28:38 +0200 Subject: [PATCH 2/9] Add themes from tB BETA 982. --- builder/themes/Classic.theme | 196 ++++++++++++++ builder/themes/Dark.theme | 505 +++++++++++++++++++++++++++++++++++ builder/themes/Light.theme | 505 +++++++++++++++++++++++++++++++++++ builder/themes/README.txt | 14 + 4 files changed, 1220 insertions(+) create mode 100644 builder/themes/Classic.theme create mode 100644 builder/themes/Dark.theme create mode 100644 builder/themes/Light.theme create mode 100644 builder/themes/README.txt diff --git a/builder/themes/Classic.theme b/builder/themes/Classic.theme new file mode 100644 index 00000000..292fcf66 --- /dev/null +++ b/builder/themes/Classic.theme @@ -0,0 +1,196 @@ + +/* + !!! DO NOT MODIFY THIS FILE !!! + SEE README.TXT +*/ + +inherits: light; + +/* main difference between this theme and the Light theme is the semantic highlighting is effectively turned off here */ +SymbolAttributeFontStyle: normal; +SymbolAttributeFontWeight: normal; +SymbolAttributeTextDecoration: none; +SymbolBuiltInDataTypeColor: #002DA6; +SymbolBuiltInDataTypeFontStyle: normal; +SymbolBuiltInDataTypeFontWeight: normal; +SymbolBuiltInDataTypeTextDecoration: none; +SymbolClassColor: #000000; +SymbolClassFontStyle: normal; +SymbolClassFontWeight: normal; +SymbolClassTextDecoration: none; +SymbolCommentColor: #00801D; +SymbolCommentFontStyle: normal; +SymbolCommentFontWeight: normal; +SymbolCommentTextDecoration: none; +SymbolConditionalCompilationDirectiveColor: #ad8c98; +SymbolConditionalCompilationDirectiveFontStyle: normal; +SymbolConditionalCompilationDirectiveFontWeight: normal; +SymbolConditionalCompilationDirectiveTextDecoration: none; +SymbolConditionalCompilationExcludedCodeColor: #989599; +SymbolConditionalCompilationExcludedCodeFontStyle: oblique; +SymbolConditionalCompilationExcludedCodeFontWeight: normal; +SymbolConditionalCompilationExcludedCodeTextDecoration: none; +SymbolConstantColor: #000000; +SymbolConstantFontStyle: normal; +SymbolConstantFontWeight: normal; +SymbolConstantTextDecoration: none; +SymbolContinuationCharacterColor: #808080; +SymbolContinuationCharacterFontStyle: normal; +SymbolContinuationCharacterFontWeight: normal; +SymbolContinuationCharacterTextDecoration: none; +SymbolDeclareFunctionColor: #002DA6; +SymbolDeclareFunctionFontStyle: normal; +SymbolDeclareFunctionFontWeight: normal; +SymbolDeclareFunctionTextDecoration: none; +SymbolDeclareSubColor: #002DA6; +SymbolDeclareSubFontStyle: normal; +SymbolDeclareSubFontWeight: normal; +SymbolDeclareSubTextDecoration: none; +SymbolEnumColor: #002DA6; +SymbolEnumFontStyle: normal; +SymbolEnumFontWeight: normal; +SymbolEnumMemberColor: #000000; +SymbolEnumMemberFontStyle: normal; +SymbolEnumMemberFontWeight: normal; +SymbolEnumMemberTextDecoration: none; +SymbolEnumTextDecoration: none; +SymbolFieldColor: #000000; +SymbolFieldFontStyle: normal; +SymbolFieldFontWeight: normal; +SymbolFieldTextDecoration: none; +SymbolFunctionColor: #000000; +SymbolFunctionFontStyle: normal; +SymbolFunctionFontWeight: normal; +SymbolFunctionTextDecoration: none; +SymbolGenericDataTypeColor: #002DA6; +SymbolGenericDataTypeFontStyle: normal; +SymbolGenericDataTypeFontWeight: normal; +SymbolGenericDataTypeTextDecoration: none; +SymbolGenericValueColor: #002DA6; +SymbolGenericValueFontStyle: normal; +SymbolGenericValueFontWeight: normal; +SymbolGenericValueTextDecoration: none; +SymbolGlobalVariablePrivateColor: #000000; +SymbolGlobalVariablePrivateFontStyle: normal; +SymbolGlobalVariablePrivateFontWeight: normal; +SymbolGlobalVariablePrivateTextDecoration: none; +SymbolGlobalVariablePublicColor: #000000; +SymbolGlobalVariablePublicFontStyle: normal; +SymbolGlobalVariablePublicFontWeight: normal; +SymbolGlobalVariablePublicTextDecoration: none; +SymbolInterfaceColor: #000000; +SymbolInterfaceFontStyle: normal; +SymbolInterfaceFontWeight: normal; +SymbolInterfaceTextDecoration: none; +SymbolKeywordColor: #002DA6; +SymbolKeywordFontStyle: normal; +SymbolKeywordFontWeight: normal; +SymbolKeywordTextDecoration: none; +SymbolLateBoundFunctionColor: #000000; +SymbolLateBoundFunctionFontStyle: normal; +SymbolLateBoundFunctionFontWeight: normal; +SymbolLateBoundFunctionTextDecoration: none; +SymbolLibraryColor: #000000; +SymbolLibraryFontStyle: normal; +SymbolLibraryFontWeight: normal; +SymbolLibraryTextDecoration: none; +SymbolLineLabelColor: #448f86; +SymbolLineLabelFontStyle: normal; +SymbolLineLabelFontWeight: normal; +SymbolLineLabelTextDecoration: underline; +SymbolLineNumberColor: #448f86; +SymbolLineNumberFontStyle: normal; +SymbolLineNumberFontWeight: normal; +SymbolLineNumberTextDecoration: underline; +SymbolLiteralBooleanColor: #002DA6; +SymbolLiteralBooleanFontStyle: normal; +SymbolLiteralBooleanFontWeight: normal; +SymbolLiteralBooleanTextDecoration: none; +SymbolLiteralDateColor: #002DA6; +SymbolLiteralDateFontStyle: normal; +SymbolLiteralDateFontWeight: normal; +SymbolLiteralDateTextDecoration: none; +SymbolLiteralEmptyColor: #002DA6; +SymbolLiteralEmptyFontStyle: normal; +SymbolLiteralEmptyFontWeight: normal; +SymbolLiteralEmptyTextDecoration: none; +SymbolLiteralNothingColor: #002DA6; +SymbolLiteralNothingFontStyle: normal; +SymbolLiteralNothingFontWeight: normal; +SymbolLiteralNothingTextDecoration: none; +SymbolLiteralNullColor: #002DA6; +SymbolLiteralNullFontStyle: normal; +SymbolLiteralNullFontWeight: normal; +SymbolLiteralNullTextDecoration: none; +SymbolLiteralNumericColor: #000000; +SymbolLiteralNumericFontStyle: normal; +SymbolLiteralNumericFontWeight: normal; +SymbolLiteralNumericTextDecoration: none; +SymbolLiteralStringColor: #000000; +SymbolLiteralStringFontStyle: normal; +SymbolLiteralStringFontWeight: normal; +SymbolLiteralStringTextDecoration: none; +SymbolMeColor: #000000; +SymbolMeFontStyle: normal; +SymbolMeFontWeight: normal; +SymbolMeTextDecoration: none; +SymbolModuleColor: #000000; +SymbolModuleFontStyle: normal; +SymbolModuleFontWeight: normal; +SymbolModuleTextDecoration: none; +SymbolMultiLineSeperatorColor: #000000; +SymbolMultiLineSeperatorFontStyle: normal; +SymbolMultiLineSeperatorFontWeight: normal; +SymbolMultiLineSeperatorTextDecoration: none; +SymbolNamedArgumentColor: #000000; +SymbolNamedArgumentFontStyle: normal; +SymbolNamedArgumentFontWeight: normal; +SymbolNamedArgumentTextDecoration: none; +SymbolNamedOperatorColor: #002DA6; +SymbolNamedOperatorFontStyle: normal; +SymbolNamedOperatorFontWeight: normal; +SymbolNamedOperatorTextDecoration: none; +SymbolOperatorColor: #000000; +SymbolOperatorFontStyle: normal; +SymbolOperatorFontWeight: normal; +SymbolOperatorTextDecoration: none; +SymbolParamByRefColor: #000000; +SymbolParamByRefFontStyle: normal; +SymbolParamByRefFontWeight: normal; +SymbolParamByRefTextDecoration: none; +SymbolParamByValColor: #000000; +SymbolParamByValFontStyle: normal; +SymbolParamByValFontWeight: normal; +SymbolParamByValTextDecoration: none; +SymbolPropertyGetColor: #000000; +SymbolPropertyGetFontStyle: normal; +SymbolPropertyGetFontWeight: normal; +SymbolPropertyGetTextDecoration: none; +SymbolPropertyLetColor: #000000; +SymbolPropertyLetFontStyle: normal; +SymbolPropertyLetFontWeight: normal; +SymbolPropertyLetTextDecoration: none; +SymbolPropertySetColor: #000000; +SymbolPropertySetFontStyle: normal; +SymbolPropertySetFontWeight: normal; +SymbolPropertySetTextDecoration: none; +SymbolReturnValueColor: #000000; +SymbolReturnValueFontStyle: normal; +SymbolReturnValueFontWeight: normal; +SymbolReturnValueTextDecoration: none; +SymbolSubColor: #002DA6; +SymbolSubFontStyle: normal; +SymbolSubFontWeight: normal; +SymbolSubTextDecoration: none; +SymbolUDTColor: #000000; +SymbolUDTFontStyle: normal; +SymbolUDTFontWeight: normal; +SymbolUDTTextDecoration: none; +SymbolVariableColor: #000000; +SymbolVariableFontStyle: normal; +SymbolVariableFontWeight: normal; +SymbolVariableTextDecoration: none; +SymbolVariableUndeclaredColor: #000000; +SymbolVariableUndeclaredFontStyle: normal; +SymbolVariableUndeclaredFontWeight: normal; +SymbolVariableUndeclaredTextDecoration: none; \ No newline at end of file diff --git a/builder/themes/Dark.theme b/builder/themes/Dark.theme new file mode 100644 index 00000000..cf8e71a5 --- /dev/null +++ b/builder/themes/Dark.theme @@ -0,0 +1,505 @@ + +/* + !!! DO NOT MODIFY THIS FILE !!! + SEE README.TXT +*/ + +BracketHighlightingLevel1Color: rgb(255, 215, 0); +BracketHighlightingLevel2Color: rgb(218, 112, 214); +BracketHighlightingLevel3Color: rgb(23, 159, 255); +CodeBlockColor: rgba(0, 0, 0, 0.5); +CodeBracketPairColorization: 0; +CodeEditorMarginBackground: #282828 !important; +CodeEditorScrollVRulerZIndex: 1; +CodeIndentGuideActiveColor: #707070; +CodeIndentGuideColor: #404040; +CodeLensBackColor: rgba(206, 182, 182, 0.047); +CodeNavDropDownsBackColor: rgb(48, 47, 47); +CodeNavDropDownsBorder: 1px solid #d3d2d2; +CodeNavDropDownsSelectColor: rgb(194, 194, 194); +CodePanelBackColor: rgb(33, 33, 33); +CodePanelBottomBorder: 1px solid black; +CodeScrollbarVerticalBackColor: #bebebe2b; +ContextMenuBackColor: rgb(68, 68, 68); +ContextMenuBackground: #eeeeee; +ContextMenuBorderBottomLeftRadius: 2px; +ContextMenuBorderBottomRightRadius: 2px; +ContextMenuBorderBottom: 1px solid #a4a4a4; +ContextMenuBorderLeft: 1px solid #ffffff; +ContextMenuBorderRight: 1px solid #a4a4a4; +ContextMenuBorderTop: 1px solid #ffffff; +ContextMenuForeColor: rgb(212, 211, 211); +ContextMenuItemHoverColor: rgb(210, 210, 210); +DebugConsoleEntryBackColor: #e5e6e826; +DebugIntellisenceBackColor: rgb(78, 77, 77); +DebugIntellisenceBoxShadow: 0 0 10px rgb(0, 0, 0); +DebugIntellisenceForeColor: ; +DebugIntellisensePartialMatchColor: rgb(123, 202, 225); +DebugLineBackColor: rgba(251, 255, 0, 0.102); +DebugTimestampColor: rgb(80, 100, 100); +DesignerBoxShadow: 0 0 18px black; +DesignerInfoBoxBackground: #e4e8ec; +DesignerInfoBoxShadow: 0 0 5px #444444; +DesignerMenuAddButtonBackColor: #5d9398; +DesignerMenuAddButtonBorderRadius: 2px; +DesignerMenuAddButtonFontWeight: normal; +DesignerMenuAddButtonMargin: 2px 0; +DesignerMenuBarBackColor: -webkit-linear-gradient(left, #e6e6e6 0%, #ffffff 48%, #ececec 100%); +DesignerMenuItemSelectedBackColor: #d3d4db; +DesignerRulerBackColor: rgb(46, 45, 45); +DesignerRulerCornerBackColor: rgb(105, 104, 104); +DesignerRulerCornerBorderRadius: 2px; +DesignerRulerCornerBorder: 1px solid rgb(0, 0, 0); +DesignerRulerCornerDotColor: rgb(214, 214, 214); +DesignerRulerCornerHeight: 18px; +DesignerRulerCornerOutline: 2px solid rgb(128, 128, 128); +DesignerRulerMarkerColor: rgb(136, 255, 188); +DesignerToolboxItemHoverBackground: rgba(128,128,128,0.5); +DesignerToolboxItemSelectedOutline: 1.5px solid rgb(135 66 103 / 28%) !important; +DiagnosticsBubblesFontSize: 110%; +DiagnosticsBubblesForeColor: white; +DiagnosticsBubblesTop: 1px; +DiagnosticsGroupBackground: rgba(200,100,100,0.2); +DiagnosticsGroupFontSize: 90% +DiagnosticsGroupFontWeight: 500; +DiagnosticsWarningIconColor: #a8a837; +DiagnosticsErrorBackColor: rgb(206 91 91 / 50%); +DiagnosticsWarningsBackColor: rgb(157 160 40 / 70%); +DiagnosticsHintsBackColor: rgba(99, 143, 95, 0.5); +DiagnosticsInfosBackColor: rgb(107 117 161 / 48%); +DialogButtonsBackColor: #fbfdff; +DialogButtonsBarBackColor: #f6f6f6; +DialogButtonFocusRectColor: #c36d91; +DialogGeneralBackColor: white; +DialogGeneralForeColor: #9d3636; +DialogGeneralPadding: 5px 0 0 0; +DialogHeaderBackColor: rgb(68 58 58); +DialogTabBarBackColor: #ffffff; +DialogTabBarBorderTop: 2px solid lightgrey; +DialogTabBarPadding: 2px 10px; +DialogTabButtonBackColor: #ffffffff; +DialogTabButtonFontSize: 85%; +DialogTabButtonPadding: 2px 20px; +DialogTabFontSize: 80%; +DialogTabForeColor: black; +DisabledIconFilter: contrast(100) grayscale(1) opacity(0.25); +DisabledIconType1Filter: contrast(0.0) brightness(2) opacity(0.3); +DockBorder: 1px solid black; +DockContainerBackground: transparent; +DockDragBottomHighight: linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.3) 100%); +DockDragLeftHighight: linear-gradient(-90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.3) 100%); +DockDragRightHighight: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.3) 100%); +DockDragTopHighight: linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.3) 100%); +ErrorSquiggleBackColor: rgba(255, 0, 0, 0.188); +ErrorTabTextColor: rgb(255 230 230) !important; +ExpandCollapseIconFilter: none; +ExpandCollapseIconSize: 10px; +FileIconFilter: none; +FindResultColor: rgba(205, 205, 226, 0.712); +FloatingPanelBorder: 1px solid #848484; +FloatingPanelBoxShadow: 0px 0px 10px black; +FormDesignerCanvasBackground2: repeating-linear-gradient(45deg,rgba(43, 41, 41, 0.38) 5px,rgba(94, 89, 81, 0.44) 8px); +FormDesignerCanvasBackground: rgb(87, 86, 86); +FrontPageBackground: rgb(235 239 241); +FrontPageIconsFilter: none; +FrontPageItemBackColor: #f5f5f5; +FrontPageItemBorderRadius: 2px; +FrontPageListBackground: #ece7e7 !important; +FrontPageSelectedItemBackColor: white !important; +FullScreenToggleBackColor: rgb(84, 84, 84); +GeneralBackColor: rgb(43, 43, 43); +GeneralIconFilter: none; +GeneralPanelBackColor: rgb(43, 43, 43); +GeneralScrollbarBackground: #f6f6f6 !important; +GeneralScrollbarBorderLeft: 1px solid #404040 !important; +GeneralScrollbarRightOffset: 1px !important; +GeneralSubTextColor: rgba(255, 255, 255, 0.5); +GeneralTextColorProminent: rgb(245, 245, 245); +GeneralTextColor: rgb(200, 200, 200); +GeneralTreeViewFontWeight: 400 !important; +GlobalSearchMatchTextColorPart: rgb(192 225 156 / 71%); +IconFilterPreviewHover: grayscale(1) brightness(2) !important; +IconHoverFilter: drop-shadow(1px 4px 5px black) brightness(1.2); +IconType1Filter: contrast(0.7) brightness(2); +IconType1HoverFilter: brightness(1.5) drop-shadow(1px 4px 5px white); +IconType2Filter: invert(1); +IconType2HoverFilter: invert(1)drop-shadow(1px 4px 5px white); +IconType3Filter: brightness(2); +IconType3HoverFilter: brightness(1.5) drop-shadow(1px 4px 5px white); +IconType4Filter: none; +IconType5Filter: none; +IconType6Filter: none; +IconType7Filter: brightness(3.4); +IconType8Filter: brightness(2); +IconType8HoverFilter: brightness(1.5) drop-shadow(1px 4px 5px white); +IconTypeUndoRedoFilter: none; +IconTypeUndoRedoHoverFilter: brightness(1.1) drop-shadow(1px 4px 5px white); +InfoBoxBackColor: rgb(82, 79, 79); +InfoBoxButtonBackColor: rgb(200, 200, 200); +InfoBoxButtonHoverBackColor: rgb(75, 76, 78); +InfoBoxIconFilter: invert(1); +InfoBoxTextColor: white; +InlineDecorationColor: rgba(192, 189, 189, 0.18); +IntellisenceBackColor: ; +IntellisenceBoxShadow: ; +IntellisenseSelectedBackColor: rgb(70, 142, 251); +IntellisenseSelectedForeColor: white; +Light_scrollButtonBackBlendMode: hard-light; +Light_scrollThumbColorHover: rgba(126, 126, 126, 0.4); +Light_scrollThumbColor: rgba(96, 96, 96, 0.4); +Light_scrollTrackColor: rgb(226 222 222); +LoadingOverlayBackColor: rgb(0,0,0); +MenuBackColor: rgb(48 38 38); +MenuDesignerBackground: -webkit-linear-gradient(top,rgb(194, 194, 194) 0%,rgb(186, 186, 186) 48%,rgb(170, 169, 169) 100%); +MenuForeColor: white; +ModalDialogBoxShadow: rgba(178, 177, 177, 0.30) 0px 0px 40px; +MonacoScrollBorderTop: 1px solid rgb(91, 91, 91); +MonacoScrollHorzBack: rgb(59, 59, 59); +NoInvertFilter: invert(1); +NotificationBoxBackColor: #eeeeee; +NotificationBoxForeColor: black; +PackageManagerHoverBackColor: rgba(255, 255, 255, 0.05) !important; +PanelBackground: #2B2B2B; +PanelBorderTop: 0; +PanelBorderBottom: 0; +PanelBorderLeft: 0; +PanelBorderRight: 0; +PanelCloseButtonBackground: transparent; +PanelCloseButtonCornerRadius: 0; +PanelCloseButtonFilter: none; +PanelCloseButtonForeColor: #d0d0d0; +PanelCloseButtonOpacity: 1; +PanelCloseButtonOutline: 0; +PanelHeaderBackColor: rgb(75 67 67); +PanelHeaderBorderBottom: 0; +PanelHeaderBorderLeft: 0; +PanelHeaderBorderRight: ; +PanelHeaderBorderTop: 1px solid rgb(100, 98, 98); +PanelHeaderFilterHover: drop-shadow(0px 0px 4px white); +PanelHeaderFontSize: 100%; +PanelHeaderGripperPaddingRight: 10px; +PanelHeaderGripperSize: 13px; +PanelHeaderIconFilter2: drop-shadow(0px 0px 1px black); +PanelHeaderIconFilter3: brightness(2.9); +PanelHeaderIconFilter4: invert(1) brightness(0.7); +PanelHeaderPadding: 2px 0 5px 0; +PanelHeaderTextColor: rgb(236, 233, 233); +PlayIconBlue: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDM4Ni45NzIgMzg2Ljk3MiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzg2Ljk3MiAzODYuOTcyOyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgPGRlZnMgaWQ9ImRlZnMzNyI+PC9kZWZzPg0KICA8ZyBpZD0iZzQiPjwvZz4NCiAgPGcgaWQ9Imc2Ij48L2c+DQogIDxnIGlkPSJnOCI+PC9nPg0KICA8ZyBpZD0iZzEwIj48L2c+DQogIDxnIGlkPSJnMTIiPjwvZz4NCiAgPGcgaWQ9ImcxNCI+PC9nPg0KICA8ZyBpZD0iZzE2Ij48L2c+DQogIDxnIGlkPSJnMTgiPjwvZz4NCiAgPGcgaWQ9ImcyMCI+PC9nPg0KICA8ZyBpZD0iZzIyIj48L2c+DQogIDxnIGlkPSJnMjQiPjwvZz4NCiAgPGcgaWQ9ImcyNiI+PC9nPg0KICA8ZyBpZD0iZzI4Ij48L2c+DQogIDxnIGlkPSJnMzAiPjwvZz4NCiAgPGcgaWQ9ImczMiI+PC9nPg0KICA8cGF0aCBzdHlsZT0iZmlsbDogcmdiKDUzLCAxMTUsIDQ5KTsgc3Ryb2tlLXdpZHRoOiAwOyIgZD0iTSAzMS45OTcgMjczLjA3MSBDIDMxLjg1MiAyMjIuNjM4IDMxLjg5NyAxNDQuNjQxIDMyLjA5MyA5OS43NDMgTCAzMi40NTIgMTguMTEzIEwgNDQuOTc0IDI0Ljc4MSBDIDEwMC44MTkgNTQuNTEzIDM0OS4yMyAxODguMzgyIDM1Mi44NjQgMTkwLjcwMSBDIDM1My45ODEgMTkxLjQxNSAzNTIuNjU5IDE5Mi4zMTggMzQyLjEzIDE5OC4wMzMgQyAzMDAuNjc1IDIyMC41MzcgNzcuNTExIDM0MC42MDYgNDkuNjI1IDM1NS40MTEgQyA0Mi41NCAzNTkuMTcxIDM1LjczNiAzNjIuODE1IDM0LjUgMzYzLjUwOCBMIDMyLjI1NiAzNjQuNzY3IEwgMzEuOTk3IDI3My4wNzEgWiIgaWQ9InBhdGgxMTQiPjwvcGF0aD4NCjwvc3ZnPg=='); +ProblemSelectedBackColor: rgba(255, 255, 0, 0.03); +ProblemSelectedBorderColor: rgb(255, 241, 0); +PropertyCategoryBack: rgba(0, 0, 0, 1); +PropertySheetControlSelectorBackground: #897b7b; +PropertySheetEntryBorder: 1px solid rgb(158, 157, 157); +PropertySheetEntryUnimplementedBackColor: rgb(148, 117, 117) !important; +PropertySheetNameBackColor: rgba(133, 126, 126, 0.62); +PropertySheetNameBackground: rgb(189, 188, 188); +PropertySheetNameBorderColor: 1px solid rgba(176, 176, 176, 0.43); +PropertySheetNodeExpanderFilter: invert(1) brightness(2); +PropertySheetValueBackColor: rgba(86, 86, 86, 0.6); +PropertySheetValueBorderColor: 1px solid rgb(96, 95, 95); +PropertySheetValueDropDownBackColor: rgb(100, 100, 100); +PropertySheetValueForeColor: white !important; +PropertySheetValueUnimplementedBackColor: rgb(132, 101, 101) !important; +ReportSectionBarBackColor: #f4f4f4; +ScrollButtonBackBlendMode: none; +ScrollThumbColor: rgba(120, 110, 110, 0.8); +ScrollTrackColor: rgb(58 58 58); +SectionHeaderBackColor: ; +SectionHeaderBorderBottom: ; +SectionHeaderBorderTop: ; +SectionHeaderTextColor: ; +SelectedTextBackColor: rgba(255, 255, 255, 0.251) !important; +SettingsFilterBarBackColor: #a28282; +SettingsHeaderBackground: #d69696; +SignatureHelpBackColor: rgb(51, 51, 51); +SignatureHelpBoxShadow: 0 0 20px black, 0 0 70px black; +SignatureHelpCodeInBackBoxColor: rgba(185, 243, 247, 0.5) !important; +SignatureHelpForeColor: rgb(237, 237, 237); +SignatureHelpHeaderColor: #815f5f; +SocialsFilter: none; +SplitterBarBottomBorder: 1px solid rgba(80,80,80,0.5); +StatusBarBackColor: rgb(75 67 67); +StatusBarBorderTop: 1px solid #6b6b6b; +StatusBarGreenBoxBackground: #4aa74a; +StatusBarGreenBoxBorderBottom: 1px solid #96b196; +StatusBarGreenBoxBorderLeft: 1px solid #a5bea5; +StatusBarGreenBoxBorderRadius: 0; +StatusBarGreenBoxBorderRight: 1px solid #769b76; +StatusBarGreenBoxBorderTop: 1px solid #a5bea5; +StatusBarGreenBoxFontSize: 90%; +StatusBarGreenBoxFontWeight: 600; +StatusBarGreenBoxForeColor: white; +StatusBarOrangeBoxBackground: #87762a; +StatusBarOrangeBoxBorderBottom: 1px solid #6b6620; +StatusBarOrangeBoxBorderLeft: 1px solid #b4ab2c; +StatusBarOrangeBoxBorderRight: 1px solid #6b6620; +StatusBarOrangeBoxBorderTop: 1px solid #b4ab2c; +StatusBarRedBoxBackground: #7e2a2a; +StatusBarRedBoxBorderBottom: 1px solid #6b2020; +StatusBarRedBoxBorderLeft: 1px solid #da3535; +StatusBarRedBoxBorderRight: 1px solid #6b2020; +StatusBarRedBoxBorderTop: 1px solid #da3535; +StatusErrorsOutline: none; +StatusHintsOutline: none; +StatusInfosOutline: none; +StatusWarningsOutline: none; +StickyScrollBackColor: rgb(56 37 65); +StopIconRed: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDMzMCAzMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMzMCAzMzA7IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzIGlkPSJkZWZzNTY5Ij48L2RlZnM+CiAgPGcgaWQ9Imc1MzYiPjwvZz4KICA8ZyBpZD0iZzUzOCI+PC9nPgogIDxnIGlkPSJnNTQwIj48L2c+CiAgPGcgaWQ9Imc1NDIiPjwvZz4KICA8ZyBpZD0iZzU0NCI+PC9nPgogIDxnIGlkPSJnNTQ2Ij48L2c+CiAgPGcgaWQ9Imc1NDgiPjwvZz4KICA8ZyBpZD0iZzU1MCI+PC9nPgogIDxnIGlkPSJnNTUyIj48L2c+CiAgPGcgaWQ9Imc1NTQiPjwvZz4KICA8ZyBpZD0iZzU1NiI+PC9nPgogIDxnIGlkPSJnNTU4Ij48L2c+CiAgPGcgaWQ9Imc1NjAiPjwvZz4KICA8ZyBpZD0iZzU2MiI+PC9nPgogIDxnIGlkPSJnNTY0Ij48L2c+CiAgPHBhdGggc3R5bGU9ImZpbGw6Izc4MjEyMTtzdHJva2Utd2lkdGg6MC40NTg5NzEiIGQ9Ik0gMzAuMDYyNTg3LDE2NSBWIDMwLjA2MjU4NyBIIDE2NSAyOTkuOTM3NDEgViAxNjUgMjk5LjkzNzQxIEggMTY1IDMwLjA2MjU4NyBaIiBpZD0icGF0aDc0MyI+PC9wYXRoPgo8L3N2Zz4='); +SymbolAttributeColor: #5c5c53; +SymbolAttributeFontStyle: normal; +SymbolAttributeFontWeight: normal; +SymbolAttributeTextDecoration: none; +SymbolBuiltInDataTypeColor: #b1551f; +SymbolBuiltInDataTypeFontStyle: normal; +SymbolBuiltInDataTypeFontWeight: normal; +SymbolBuiltInDataTypeTextDecoration: none; +SymbolClassColor: #e4c685; +SymbolClassFontStyle: normal; +SymbolClassFontWeight: normal; +SymbolClassTextDecoration: none; +SymbolCommentColor: #448a63; +SymbolCommentFontStyle: normal; +SymbolCommentFontWeight: normal; +SymbolCommentTextDecoration: none; +SymbolConditionalCompilationDirectiveColor: #ad8c98; +SymbolConditionalCompilationDirectiveFontStyle: normal; +SymbolConditionalCompilationDirectiveFontWeight: normal; +SymbolConditionalCompilationDirectiveTextDecoration: none; +SymbolConditionalCompilationExcludedCodeColor: #989599; +SymbolConditionalCompilationExcludedCodeFontStyle: oblique; +SymbolConditionalCompilationExcludedCodeFontWeight: normal; +SymbolConditionalCompilationExcludedCodeTextDecoration: none; +SymbolConstantColor: #6089b4; +SymbolConstantFontStyle: normal; +SymbolConstantFontWeight: normal; +SymbolConstantTextDecoration: none; +SymbolContinuationCharacterColor: #808080; +SymbolContinuationCharacterFontStyle: normal; +SymbolContinuationCharacterFontWeight: normal; +SymbolContinuationCharacterTextDecoration: none; +SymbolDeclareFunctionColor: #bb956a; +SymbolDeclareFunctionFontStyle: normal; +SymbolDeclareFunctionFontWeight: normal; +SymbolDeclareFunctionTextDecoration: none; +SymbolDeclareSubColor: #bb956a; +SymbolDeclareSubFontStyle: normal; +SymbolDeclareSubFontWeight: normal; +SymbolDeclareSubTextDecoration: none; +SymbolEnumColor: #738dc5; +SymbolEnumFontStyle: normal; +SymbolEnumFontWeight: normal; +SymbolEnumMemberColor: #a1adc7; +SymbolEnumMemberFontStyle: normal; +SymbolEnumMemberFontWeight: normal; +SymbolEnumMemberTextDecoration: none; +SymbolEnumTextDecoration: none; +SymbolFieldColor: #f59e1b; +SymbolFieldFontStyle: normal; +SymbolFieldFontWeight: normal; +SymbolFieldTextDecoration: none; +SymbolFunctionColor: #cf9a5d; +SymbolFunctionFontStyle: normal; +SymbolFunctionFontWeight: normal; +SymbolFunctionTextDecoration: none; +SymbolGenericDataTypeColor: #eeda83; +SymbolGenericDataTypeFontStyle: normal; +SymbolGenericDataTypeFontWeight: normal; +SymbolGenericDataTypeTextDecoration: none; +SymbolGenericValueColor: #86ee83; +SymbolGenericValueFontStyle: normal; +SymbolGenericValueFontWeight: normal; +SymbolGenericValueTextDecoration: none; +SymbolGlobalVariablePrivateColor: #f38096; +SymbolGlobalVariablePrivateFontStyle: normal; +SymbolGlobalVariablePrivateFontWeight: normal; +SymbolGlobalVariablePrivateTextDecoration: none; +SymbolGlobalVariablePublicColor: #d34056; +SymbolGlobalVariablePublicFontStyle: normal; +SymbolGlobalVariablePublicFontWeight: normal; +SymbolGlobalVariablePublicTextDecoration: none; +SymbolInterfaceColor: #ab3b4d; +SymbolInterfaceFontStyle: normal; +SymbolInterfaceFontWeight: normal; +SymbolInterfaceTextDecoration: none; +SymbolKeywordColor: #6c8eda; +SymbolKeywordFontStyle: normal; +SymbolKeywordFontWeight: normal; +SymbolKeywordTextDecoration: none; +SymbolLateBoundFunctionColor: #e7ac5f; +SymbolLateBoundFunctionFontStyle: normal; +SymbolLateBoundFunctionFontWeight: normal; +SymbolLateBoundFunctionTextDecoration: none; +SymbolLibraryColor: #bb6464; +SymbolLibraryFontStyle: normal; +SymbolLibraryFontWeight: normal; +SymbolLibraryTextDecoration: none; +SymbolLineLabelColor: #ccc6be; +SymbolLineLabelFontStyle: normal; +SymbolLineLabelFontWeight: normal; +SymbolLineLabelTextDecoration: underline; +SymbolLineNumberColor: #ccc6be; +SymbolLineNumberFontStyle: normal; +SymbolLineNumberFontWeight: normal; +SymbolLineNumberTextDecoration: underline; +SymbolLiteralBooleanColor: #c495d3; +SymbolLiteralBooleanFontStyle: normal; +SymbolLiteralBooleanFontWeight: normal; +SymbolLiteralBooleanTextDecoration: none; +SymbolLiteralDateColor: #c495d3; +SymbolLiteralDateFontStyle: normal; +SymbolLiteralDateFontWeight: normal; +SymbolLiteralDateTextDecoration: none; +SymbolLiteralEmptyColor: #c495d3; +SymbolLiteralEmptyFontStyle: normal; +SymbolLiteralEmptyFontWeight: normal; +SymbolLiteralEmptyTextDecoration: none; +SymbolLiteralNothingColor: #c495d3; +SymbolLiteralNothingFontStyle: normal; +SymbolLiteralNothingFontWeight: normal; +SymbolLiteralNothingTextDecoration: none; +SymbolLiteralNullColor: #c495d3; +SymbolLiteralNullFontStyle: normal; +SymbolLiteralNullFontWeight: normal; +SymbolLiteralNullTextDecoration: none; +SymbolLiteralNumericColor: #aeca89; +SymbolLiteralNumericFontStyle: normal; +SymbolLiteralNumericFontWeight: normal; +SymbolLiteralNumericTextDecoration: none; +SymbolLiteralStringColor: #aeca89; +SymbolLiteralStringFontStyle: oblique; +SymbolLiteralStringFontWeight: normal; +SymbolLiteralStringTextDecoration: none; +SymbolMeColor: #a13838; +SymbolMeFontStyle: normal; +SymbolMeFontWeight: normal; +SymbolMeTextDecoration: none; +SymbolModuleColor: #a8a887; +SymbolModuleFontStyle: normal; +SymbolModuleFontWeight: normal; +SymbolModuleTextDecoration: none; +SymbolMultiLineSeperatorColor: #74384c; +SymbolMultiLineSeperatorFontStyle: normal; +SymbolMultiLineSeperatorFontWeight: normal; +SymbolMultiLineSeperatorTextDecoration: none; +SymbolNamedArgumentColor: #74384c; +SymbolNamedArgumentFontStyle: normal; +SymbolNamedArgumentFontWeight: normal; +SymbolNamedArgumentTextDecoration: none; +SymbolNamedOperatorColor: #80a1a5; +SymbolNamedOperatorFontStyle: normal; +SymbolNamedOperatorFontWeight: normal; +SymbolNamedOperatorTextDecoration: none; +SymbolOperatorColor: #80a1a5; +SymbolOperatorFontStyle: normal; +SymbolOperatorFontWeight: normal; +SymbolOperatorTextDecoration: none; +SymbolParamByRefColor: #9b79b3; +SymbolParamByRefFontStyle: normal; +SymbolParamByRefFontWeight: normal; +SymbolParamByRefTextDecoration: none; +SymbolParamByValColor: #9b79b3; +SymbolParamByValFontStyle: normal; +SymbolParamByValFontWeight: normal; +SymbolParamByValTextDecoration: none; +SymbolPropertyGetColor: #864f0f; +SymbolPropertyGetFontStyle: normal; +SymbolPropertyGetFontWeight: normal; +SymbolPropertyGetTextDecoration: none; +SymbolPropertyLetColor: #864f0f; +SymbolPropertyLetFontStyle: normal; +SymbolPropertyLetFontWeight: normal; +SymbolPropertyLetTextDecoration: none; +SymbolPropertySetColor: #864f0f; +SymbolPropertySetFontStyle: normal; +SymbolPropertySetFontWeight: normal; +SymbolPropertySetTextDecoration: none; +SymbolReturnValueColor: #ee8391; +SymbolReturnValueFontStyle: normal; +SymbolReturnValueFontWeight: normal; +SymbolReturnValueTextDecoration: none; +SymbolSubColor: #cf9a5d; +SymbolSubFontStyle: normal; +SymbolSubFontWeight: normal; +SymbolSubTextDecoration: none; +SymbolUDTColor: #a5630d; +SymbolUDTFontStyle: normal; +SymbolUDTFontWeight: normal; +SymbolUDTTextDecoration: none; +SymbolVariableColor: #8b8b52; +SymbolVariableFontStyle: normal; +SymbolVariableFontWeight: normal; +SymbolVariableTextDecoration: none; +SymbolVariableUndeclaredColor: #b9929c; +SymbolVariableUndeclaredFontStyle: normal; +SymbolVariableUndeclaredFontWeight: normal; +SymbolVariableUndeclaredTextDecoration: none; +SystemIconsFullScreenFilter: none; +TabArrowsBackColor: #5e5c5c; +TabArrowsForeColor: white; +TabHoverBackColor: rgb(108, 108, 108); +TabIconFilter: none; +TabItemSelectedTextColor: white; +TabsBackColor: rgb(43, 43, 43); +TabsBorderBottom: 0; +TabDirtyMarker: radial-gradient(ellipse at center, rgb(255, 255, 255) 0%,rgb(255, 255, 255) 30%, transparent 35%); +TabSelectedBackColor: rgb(135 107 107); +TabSelectedHighlightBorderLeft: 1px solid rgb(142, 109, 109); +TabSelectedHighlightBorderRight: 1px solid rgb(142, 109, 109); +TabSelectedHighlightBorderTop: 1px solid rgb(142, 109, 109); +TabSelectedHoverBackColor: rgb(135 107 107); +TabUnselectedBackColor: rgb(71, 71, 71); +TabUnselectedHighlightBorderLeft: 0; +TabUnselectedHighlightBorderRight: 0; +TabUnselectedHighlightBorderTop: 0; +ToolbarBackground: #4B4343; +ToolbarDropDownBorder: 1px solid #bdbdbd; +ToolbarPreviewButtonColor: #fe3838c2; +ToolWindowBodyForeColor: rgb(214, 214, 214); +TreeItemHoverBackColor: rgb(113, 113, 113); +TreeItemSelectedBackColor: rgb(70, 70, 70); +TreeItemSelectedForeColor: white; +VariablesPanelHoverTooltipBackColor: rgb(255 255 255); +VariablesPanelVariableNameFontWeight: bold; +VariablesPanelVariableNameForeColor: #c586c0; +VirtualDocBackColor: rgb(36, 33, 33); +VirtualDocForeColor: rgb(255, 255, 255); +VirtualDocMarkdownBackColor: rgb(36, 33, 33); +VirtualDocMarkdownBorderColor: rgb(51, 51, 51); +VirtualDocMarkdownForeColor: white; +VirtualDocMarkdownHeaderColor: rgb(255, 255, 112); +WarningTreeNodeColor: rgb(206, 186, 99); + +/* These icon properties support CSS data URIs, e.g. Something = url("data:image;base64,BASE64DATAHERE"); */ +ToolboxIconPointer: ; +ToolboxIconPictureBox: ; +ToolboxIconLabel: ; +ToolboxIconTextBox: ; +ToolboxIconFrame: ; +ToolboxIconCommandButton: ; +ToolboxIconCheckBox: ; +ToolboxIconOptionButton: ; +ToolboxIconComboBox: ; +ToolboxIconListBox: ; +ToolboxIconHScrollBar: ; +ToolboxIconVScrollBar: ; +ToolboxIconTimer: ; +ToolboxIconDriveListBox: ; +ToolboxIconDirListBox: ; +ToolboxIconFileListBox: ; +ToolboxIconShape: ; +ToolboxIconLine: ; +ToolboxIconImage: ; +ToolboxIconCheckMark: ; +ToolboxIconQRCode: ; +ToolboxIconData: ; +ToolboxIconOLE: ; +ToolboxIconMultiFrame: ; +ToolboxIconWebView2PackageWebView2: ; +ToolboxIconCOMMONCONTROLSDTPicker: ; +ToolboxIconCOMMONCONTROLSImageList: ; +ToolboxIconCOMMONCONTROLSListView: ; +ToolboxIconCOMMONCONTROLSMonthView: ; +ToolboxIconCOMMONCONTROLSProgressBar: ; +ToolboxIconCOMMONCONTROLSSlider: ; +ToolboxIconCOMMONCONTROLSTreeView: ; +ToolboxIconCOMMONCONTROLSUpDown: ; \ No newline at end of file diff --git a/builder/themes/Light.theme b/builder/themes/Light.theme new file mode 100644 index 00000000..31672aca --- /dev/null +++ b/builder/themes/Light.theme @@ -0,0 +1,505 @@ + +/* + !!! DO NOT MODIFY THIS FILE !!! + SEE README.TXT +*/ + +BracketHighlightingLevel1Color: rgb(119, 105, 28); +BracketHighlightingLevel2Color: rgb(218, 112, 214); +BracketHighlightingLevel3Color: rgb(23, 159, 255); +CodeBlockColor: rgba(0, 0, 0, 0.15); +CodeBracketPairColorization: 0; +CodeEditorMarginBackground: #f8f8f8 !important; +CodeEditorScrollVRulerZIndex: 1; +CodeIndentGuideActiveColor: #C0C0C0; +CodeIndentGuideColor: #D8D8D8; +CodeLensBackColor: rgba(206, 182, 182, 0.047); +CodeNavDropDownsBackColor: #f4f4f4; +CodeNavDropDownsBorder: 1px solid #d3d2d2; +CodeNavDropDownsSelectColor: white; +CodePanelBackColor: ; +CodePanelBottomBorder: ; +CodeScrollbarVerticalBackColor: #efefef; +ContextMenuBackColor: rgb(68, 68, 68); +ContextMenuBackground: #f0f0f0; +ContextMenuBorderBottomLeftRadius: 2px; +ContextMenuBorderBottomRightRadius: 2px; +ContextMenuBorderBottom: 1px solid #a4a4a4; +ContextMenuBorderLeft: 1px solid #ffffff; +ContextMenuBorderRight: 1px solid #a4a4a4; +ContextMenuBorderTop: 1px solid #ffffff; +ContextMenuForeColor: rgb(212, 211, 211); +ContextMenuItemHoverColor: #bdd1d4; +DebugConsoleEntryBackColor: #e5e6e826; +DebugIntellisenceBackColor: rgb(250, 250, 250); +DebugIntellisenceBoxShadow: 0 0 1px black; +DebugIntellisenceForeColor: black; +DebugIntellisensePartialMatchColor: rgb(53, 112, 130); +DebugLineBackColor: rgb(237 239 79 / 78%); +DebugTimestampColor: rgb(80, 100, 100); +DesignerBoxShadow: 0 0 18px #393939; +DesignerInfoBoxBackground: #e4e8ec; +DesignerInfoBoxShadow: 0 0 5px #444444; +DesignerMenuAddButtonBackColor: #5d9398; +DesignerMenuAddButtonBorderRadius: 2px; +DesignerMenuAddButtonFontWeight: normal; +DesignerMenuAddButtonMargin: 2px 0; +DesignerMenuBarBackColor: -webkit-linear-gradient(left, #e6e6e6 0%, #ffffff 48%, #ececec 100%); +DesignerMenuItemSelectedBackColor: #d3d4db; +DesignerRulerBackColor: rgb(255, 255, 255); +DesignerRulerCornerBackColor: rgb(236, 236, 236); +DesignerRulerCornerBorderRadius: 0px; +DesignerRulerCornerBorder: 1px solid rgb(255, 255, 255); +DesignerRulerCornerDotColor: rgb(121, 121, 121); +DesignerRulerCornerHeight: 19px; +DesignerRulerCornerOutline: 2px solid rgb(200, 196, 196); +DesignerRulerMarkerColor: rgb(84, 172, 122); +DesignerToolboxItemHoverBackground: rgba(178,178,178,0.3); +DesignerToolboxItemSelectedOutline: 1.5px solid rgb(72 66 135 / 28%) !important; +DiagnosticsBubblesFontSize: 110%; +DiagnosticsBubblesForeColor: white; +DiagnosticsBubblesTop: 0; +DiagnosticsGroupBackground: #e6e6ea; +DiagnosticsGroupFontSize: 90% +DiagnosticsGroupFontWeight: 500; +DiagnosticsWarningIconColor: #a8a837; +DiagnosticsErrorBackColor: rgb(208 101 101 / 50%); +DiagnosticsWarningsBackColor: rgb(193 196 69 / 70%); +DiagnosticsHintsBackColor: rgb(84 162 77 / 50%); +DiagnosticsInfosBackColor: rgb(144 156 213 / 48%); +DialogButtonsBackColor: #fbfdff; +DialogButtonsBarBackColor: #f6f6f6; +DialogButtonFocusRectColor: #6d6dc3; +DialogGeneralBackColor: white; +DialogGeneralForeColor: black; +DialogGeneralPadding: 5px 0 0 0; +DialogHeaderBackColor: #006EC8; +DialogTabBarBackColor: #ffffff; +DialogTabBarBorderTop: 2px solid blue; +DialogTabBarPadding: 2px 10px; +DialogTabButtonBackColor: #ffffff; +DialogTabButtonFontSize: 85%; +DialogTabButtonPadding: 2px 10px; +DialogTabFontSize: 80%; +DialogTabForeColor: black; +DisabledIconFilter: contrast(100) grayscale(1) invert(1) opacity(0.4); +DisabledIconType1Filter: contrast(0.0) brightness(0) opacity(0.3); +DockBorder: 4px solid #eCeCeC; +DockContainerBackground: rgb(238, 238, 238); +DockDragBottomHighight: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 70%, rgba(0,0,0,0.3) 100%); +DockDragLeftHighight: linear-gradient(-90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 70%, rgba(0,0,0,0.3) 100%); +DockDragRightHighight: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 70%, rgba(0,0,0,0.3) 100%); +DockDragTopHighight: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 70%, rgba(0,0,0,0.3) 100%); +ErrorSquiggleBackColor: rgb(255 0 0 / 6%); +ErrorTabTextColor: black; +ExpandCollapseIconFilter: brightness(0.5); +ExpandCollapseIconSize: 10px; +FileIconFilter: brightness(1.3); +FindResultColor: #0048ff; +FloatingPanelBorder: 0; +FloatingPanelBoxShadow: 0px 0px 10px #b1b1b1; +FormDesignerCanvasBackground2: repeating-linear-gradient(45deg, rgb(139 165 207 / 28%) 5px, rgb(235 237 238 / 32%) 11px); +FormDesignerCanvasBackground: rgb(219, 219, 219); +FrontPageBackground: rgb(235 239 241); +FrontPageIconsFilter: hue-rotate(230deg) drop-shadow(2px 4px 9px #D0D0D0); +FrontPageItemBackColor: #f5f5f5; +FrontPageItemBorderRadius: 2px; +FrontPageListBackground: #ffffff !important; +FrontPageSelectedItemBackColor: white !important; +FullScreenToggleBackColor: rgb(84, 84, 84); +GeneralBackColor: rgb(250, 250, 250); +GeneralIconFilter: none; +GeneralPanelBackColor: rgb(255, 255, 255); +GeneralScrollbarBackground: #f6f6f6 !important; +GeneralScrollbarBorderLeft: 1px solid #e0e0e0 !important; +GeneralScrollbarRightOffset: 1px !important; +GeneralSubTextColor: rgba(128, 128, 128, 0.75); +GeneralTextColorProminent: rgb(245, 245, 245); +GeneralTextColor: black; +GeneralTreeViewFontWeight: 400 !important; +GlobalSearchMatchTextColorPart: rgba(0, 0, 0, 0.71); +IconFilterPreviewHover: grayscale(1) invert(0.6) brightness(1.2) !important; +IconHoverFilter: contrast(1) brightness(0.7) drop-shadow(1px 4px 5px white); +IconType1Filter: contrast(0.5) brightness(1.1); +IconType1HoverFilter: contrast(1.5); +IconType2Filter: invert(1) contrast(0.5) brightness(0.5); +IconType2HoverFilter: invert(1)drop-shadow(1px 4px 5px white); +IconType3Filter: contrast(0.5) brightness(0.2); +IconType3HoverFilter: brightness(1.1) invert(1) drop-shadow(1px 4px 5px white); +IconType4Filter: contrast(1) brightness(0.8); +IconType5Filter: contrast(0.4) brightness(0.5); +IconType6Filter: hue-rotate(20deg) contrast(1.5) brightness(0.6); +IconType7Filter: none; +IconType8Filter: contrast(0.8) brightness(1.5); +IconType8HoverFilter: brightness(1.1) drop-shadow(1px 4px 5px white); +IconTypeUndoRedoFilter: invert(1) contrast(0.5) brightness(0.5); +IconTypeUndoRedoHoverFilter: brightness(1) invert(0.9) drop-shadow(0px 0px 3px rgb(255, 255, 255)); +InfoBoxBackColor: rgb(82, 79, 79); +InfoBoxButtonBackColor: rgb(200, 200, 200); +InfoBoxButtonHoverBackColor: rgb(180, 180, 180); +InfoBoxIconFilter: invert(1); +InfoBoxTextColor: white; +InlineDecorationColor: #00000040; +IntellisenceBackColor: rgb(250, 250, 250); +IntellisenceBoxShadow: 0 0 1px black; +IntellisenseSelectedBackColor: rgb(173, 199, 239); +IntellisenseSelectedForeColor: rgb(98, 93, 93); +Light_scrollButtonBackBlendMode: hard-light; +Light_scrollThumbColorHover: rgba(126, 126, 126, 0.4); +Light_scrollThumbColor: rgba(96, 96, 96, 0.4); +Light_scrollTrackColor: #F0F0F0; +LoadingOverlayBackColor: rgb(0,0,0); +MenuBackColor: #006EC8; +MenuDesignerBackground: -webkit-linear-gradient(top, rgb(235 234 234) 0%, rgb(213, 211, 211) 88%, rgb(203, 203, 203) 100%); +MenuForeColor: white; +ModalDialogBoxShadow: rgba(0, 0, 0, 0.24) 0px 0px 40px; +MonacoScrollBorderTop: 1px solid rgb(209, 207, 207); +MonacoScrollHorzBack: rgb(242, 242, 242); +NoInvertFilter: ; +NotificationBoxBackColor: #eeeeee; +NotificationBoxForeColor: black; +PackageManagerHoverBackColor: rgba(255, 255, 255, 0.05) !important; +PanelBackground: white; +PanelBorderTop: 0; +PanelBorderBottom: 1px solid lightgrey; +PanelBorderLeft: 1px solid #bfbfbf; +PanelBorderRight: 1px solid lightgrey; +PanelCloseButtonBackground: #F0F0F0; +PanelCloseButtonCornerRadius: 2px; +PanelCloseButtonFilter: none; +PanelCloseButtonForeColor: #525252; +PanelCloseButtonOpacity: 1; +PanelCloseButtonOutline: 1px solid #a3a3a3; +PanelHeaderBackColor: #BFCDDB; +PanelHeaderBorderBottom: 0; +PanelHeaderBorderLeft: 1px solid #a8a8a8; +PanelHeaderBorderRight: ; +PanelHeaderBorderTop: 1px solid #f4f4f4; +PanelHeaderFilterHover: drop-shadow(0px 0px 4px white); +PanelHeaderFontSize: 85%; +PanelHeaderGripperPaddingRight: 10px; +PanelHeaderGripperSize: 13px; +PanelHeaderIconFilter2: invert(1) brightness(0.5); +PanelHeaderIconFilter3: brightness(0.1); +PanelHeaderIconFilter4: invert(1.5) brightness(0.36); +PanelHeaderPadding: 2px 0 3px 0; +PanelHeaderTextColor: rgb(56 56 56); +PlayIconBlue: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDM4Ni45NzIgMzg2Ljk3MiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMzg2Ljk3MiAzODYuOTcyOyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCiAgPGRlZnMgaWQ9ImRlZnMzNyI+PC9kZWZzPg0KICA8ZyBpZD0iZzQiPjwvZz4NCiAgPGcgaWQ9Imc2Ij48L2c+DQogIDxnIGlkPSJnOCI+PC9nPg0KICA8ZyBpZD0iZzEwIj48L2c+DQogIDxnIGlkPSJnMTIiPjwvZz4NCiAgPGcgaWQ9ImcxNCI+PC9nPg0KICA8ZyBpZD0iZzE2Ij48L2c+DQogIDxnIGlkPSJnMTgiPjwvZz4NCiAgPGcgaWQ9ImcyMCI+PC9nPg0KICA8ZyBpZD0iZzIyIj48L2c+DQogIDxnIGlkPSJnMjQiPjwvZz4NCiAgPGcgaWQ9ImcyNiI+PC9nPg0KICA8ZyBpZD0iZzI4Ij48L2c+DQogIDxnIGlkPSJnMzAiPjwvZz4NCiAgPGcgaWQ9ImczMiI+PC9nPg0KICA8cGF0aCBzdHlsZT0iZmlsbDogcmdiKDUzLCAxMTUsIDQ5KTsgc3Ryb2tlLXdpZHRoOiAwOyIgZD0iTSAzMS45OTcgMjczLjA3MSBDIDMxLjg1MiAyMjIuNjM4IDMxLjg5NyAxNDQuNjQxIDMyLjA5MyA5OS43NDMgTCAzMi40NTIgMTguMTEzIEwgNDQuOTc0IDI0Ljc4MSBDIDEwMC44MTkgNTQuNTEzIDM0OS4yMyAxODguMzgyIDM1Mi44NjQgMTkwLjcwMSBDIDM1My45ODEgMTkxLjQxNSAzNTIuNjU5IDE5Mi4zMTggMzQyLjEzIDE5OC4wMzMgQyAzMDAuNjc1IDIyMC41MzcgNzcuNTExIDM0MC42MDYgNDkuNjI1IDM1NS40MTEgQyA0Mi41NCAzNTkuMTcxIDM1LjczNiAzNjIuODE1IDM0LjUgMzYzLjUwOCBMIDMyLjI1NiAzNjQuNzY3IEwgMzEuOTk3IDI3My4wNzEgWiIgaWQ9InBhdGgxMTQiPjwvcGF0aD4NCjwvc3ZnPg=='); +ProblemSelectedBackColor: rgba(255, 151, 0, 0.06); +ProblemSelectedBorderColor: rgb(255, 122, 0); +PropertyCategoryBack: #98a4ad; +PropertySheetControlSelectorBackground: #dce4e7; +PropertySheetEntryBorder: 1px solid rgb(217 215 215); +PropertySheetEntryUnimplementedBackColor: rgb(226, 217, 217) !important; +PropertySheetNameBackColor: #ececec; +PropertySheetNameBackground: rgb(233 231 231); +PropertySheetNameBorderColor: 1px solid rgb(207, 205, 205); +PropertySheetNodeExpanderFilter: none; +PropertySheetValueBackColor: white; +PropertySheetValueBorderColor: 1px solid rgb(213, 210, 210); +PropertySheetValueDropDownBackColor: rgb(204, 203, 203); +PropertySheetValueForeColor: black; +PropertySheetValueUnimplementedBackColor: rgb(240, 232, 232) !important; +ReportSectionBarBackColor: #f4f4f4; +ScrollButtonBackBlendMode: hard-light; +ScrollThumbColor: rgba(190, 200, 200, 0.8); +ScrollTrackColor: #F0F0F0; +SectionHeaderBackColor: rgb(226 219 219); +SectionHeaderBorderBottom: 1px solid rgb(154, 154, 154); +SectionHeaderBorderTop: 1px solid rgb(223, 222, 222); +SectionHeaderTextColor: rgb(91 89 89); +SelectedTextBackColor: rgb(36 128 195 / 25%) !important; +SettingsFilterBarBackColor: #c8d7e5; +SettingsHeaderBackground: #96c3d6; +SignatureHelpBackColor: rgb(245, 245, 245); +SignatureHelpBoxShadow: 0 0 20px rgb(162, 162, 162), 0 0 70px rgb(188, 188, 188); +SignatureHelpCodeInBackBoxColor: rgba(185, 243, 247, 0.5) !important; +SignatureHelpForeColor: rgb(14, 14, 14); +SignatureHelpHeaderColor: #3a4fab; +SocialsFilter: invert(1) contrast(0.1); +SplitterBarBottomBorder: 1px solid #D0D0D0; +StatusBarBackColor: rgb(223 223 223); +StatusBarBorderTop: 1px solid white; +StatusBarGreenBoxBackground: #4e8c8f; +StatusBarGreenBoxBorderBottom: 1px solid #9babc4; +StatusBarGreenBoxBorderLeft: 1px solid #7b8ba4; +StatusBarGreenBoxBorderRadius: 0; +StatusBarGreenBoxBorderRight: 1px solid #9babc4; +StatusBarGreenBoxBorderTop: 1px solid #7b8ba4; +StatusBarGreenBoxFontSize: 90%; +StatusBarGreenBoxFontWeight: 600; +StatusBarGreenBoxForeColor: white; +StatusBarOrangeBoxBackground: #d19754; +StatusBarOrangeBoxBorderBottom: 1px solid #6b6620; +StatusBarOrangeBoxBorderLeft: 1px solid #b4ab2c; +StatusBarOrangeBoxBorderRight: 1px solid #6b6620; +StatusBarOrangeBoxBorderTop: 1px solid #b4ab2c; +StatusBarRedBoxBackground: #ff4d4d; +StatusBarRedBoxBorderBottom: 1px solid #6b2020; +StatusBarRedBoxBorderLeft: 1px solid #da3535; +StatusBarRedBoxBorderRight: 1px solid #6b2020; +StatusBarRedBoxBorderTop: 1px solid #da3535; +StatusErrorsOutline: 1px solid rgb(140, 84, 84); +StatusHintsOutline: 1px solid rgb(4, 49, 6); +StatusInfosOutline: 1px solid rgb(9, 0, 50); +StatusWarningsOutline: 1px solid rgb(140, 140, 84); +StickyScrollBackColor: rgb(241 223 250); +StopIconRed: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDMzMCAzMzAiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMzMCAzMzA7IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxkZWZzIGlkPSJkZWZzNTY5Ij48L2RlZnM+CiAgPGcgaWQ9Imc1MzYiPjwvZz4KICA8ZyBpZD0iZzUzOCI+PC9nPgogIDxnIGlkPSJnNTQwIj48L2c+CiAgPGcgaWQ9Imc1NDIiPjwvZz4KICA8ZyBpZD0iZzU0NCI+PC9nPgogIDxnIGlkPSJnNTQ2Ij48L2c+CiAgPGcgaWQ9Imc1NDgiPjwvZz4KICA8ZyBpZD0iZzU1MCI+PC9nPgogIDxnIGlkPSJnNTUyIj48L2c+CiAgPGcgaWQ9Imc1NTQiPjwvZz4KICA8ZyBpZD0iZzU1NiI+PC9nPgogIDxnIGlkPSJnNTU4Ij48L2c+CiAgPGcgaWQ9Imc1NjAiPjwvZz4KICA8ZyBpZD0iZzU2MiI+PC9nPgogIDxnIGlkPSJnNTY0Ij48L2c+CiAgPHBhdGggc3R5bGU9ImZpbGw6Izc4MjEyMTtzdHJva2Utd2lkdGg6MC40NTg5NzEiIGQ9Ik0gMzAuMDYyNTg3LDE2NSBWIDMwLjA2MjU4NyBIIDE2NSAyOTkuOTM3NDEgViAxNjUgMjk5LjkzNzQxIEggMTY1IDMwLjA2MjU4NyBaIiBpZD0icGF0aDc0MyI+PC9wYXRoPgo8L3N2Zz4='); +SymbolAttributeColor: #bfbfbf; +SymbolAttributeFontStyle: normal; +SymbolAttributeFontWeight: normal; +SymbolAttributeTextDecoration: none; +SymbolBuiltInDataTypeColor: #b1551f; +SymbolBuiltInDataTypeFontStyle: normal; +SymbolBuiltInDataTypeFontWeight: normal; +SymbolBuiltInDataTypeTextDecoration: none; +SymbolClassColor: #a87300; +SymbolClassFontStyle: normal; +SymbolClassFontWeight: normal; +SymbolClassTextDecoration: none; +SymbolCommentColor: #448a63; +SymbolCommentFontStyle: normal; +SymbolCommentFontWeight: normal; +SymbolCommentTextDecoration: none; +SymbolConditionalCompilationDirectiveColor: #ad8c98; +SymbolConditionalCompilationDirectiveFontStyle: normal; +SymbolConditionalCompilationDirectiveFontWeight: normal; +SymbolConditionalCompilationDirectiveTextDecoration: none; +SymbolConditionalCompilationExcludedCodeColor: #989599; +SymbolConditionalCompilationExcludedCodeFontStyle: oblique; +SymbolConditionalCompilationExcludedCodeFontWeight: normal; +SymbolConditionalCompilationExcludedCodeTextDecoration: none; +SymbolConstantColor: #6089b4; +SymbolConstantFontStyle: normal; +SymbolConstantFontWeight: normal; +SymbolConstantTextDecoration: none; +SymbolContinuationCharacterColor: #808080; +SymbolContinuationCharacterFontStyle: normal; +SymbolContinuationCharacterFontWeight: normal; +SymbolContinuationCharacterTextDecoration: none; +SymbolDeclareFunctionColor: #bb956a; +SymbolDeclareFunctionFontStyle: normal; +SymbolDeclareFunctionFontWeight: normal; +SymbolDeclareFunctionTextDecoration: none; +SymbolDeclareSubColor: #bb956a; +SymbolDeclareSubFontStyle: normal; +SymbolDeclareSubFontWeight: normal; +SymbolDeclareSubTextDecoration: none; +SymbolEnumColor: #2360e9; +SymbolEnumFontStyle: normal; +SymbolEnumFontWeight: normal; +SymbolEnumMemberColor: #205ee6; +SymbolEnumMemberFontStyle: normal; +SymbolEnumMemberFontWeight: normal; +SymbolEnumMemberTextDecoration: none; +SymbolEnumTextDecoration: none; +SymbolFieldColor: #ea8c00; +SymbolFieldFontStyle: normal; +SymbolFieldFontWeight: normal; +SymbolFieldTextDecoration: none; +SymbolFunctionColor: #b96300; +SymbolFunctionFontStyle: normal; +SymbolFunctionFontWeight: normal; +SymbolFunctionTextDecoration: none; +SymbolGenericDataTypeColor: #eeda83; +SymbolGenericDataTypeFontStyle: normal; +SymbolGenericDataTypeFontWeight: normal; +SymbolGenericDataTypeTextDecoration: none; +SymbolGenericValueColor: #86ee83; +SymbolGenericValueFontStyle: normal; +SymbolGenericValueFontWeight: normal; +SymbolGenericValueTextDecoration: none; +SymbolGlobalVariablePrivateColor: #f38096; +SymbolGlobalVariablePrivateFontStyle: normal; +SymbolGlobalVariablePrivateFontWeight: normal; +SymbolGlobalVariablePrivateTextDecoration: none; +SymbolGlobalVariablePublicColor: #d34056; +SymbolGlobalVariablePublicFontStyle: normal; +SymbolGlobalVariablePublicFontWeight: normal; +SymbolGlobalVariablePublicTextDecoration: none; +SymbolInterfaceColor: #ab3b4d; +SymbolInterfaceFontStyle: normal; +SymbolInterfaceFontWeight: normal; +SymbolInterfaceTextDecoration: none; +SymbolKeywordColor: #385ba9; +SymbolKeywordFontStyle: normal; +SymbolKeywordFontWeight: normal; +SymbolKeywordTextDecoration: none; +SymbolLateBoundFunctionColor: #e7ac5f; +SymbolLateBoundFunctionFontStyle: normal; +SymbolLateBoundFunctionFontWeight: normal; +SymbolLateBoundFunctionTextDecoration: none; +SymbolLibraryColor: #bb6464; +SymbolLibraryFontStyle: normal; +SymbolLibraryFontWeight: normal; +SymbolLibraryTextDecoration: none; +SymbolLineLabelColor: #a6a6a6; +SymbolLineLabelFontStyle: normal; +SymbolLineLabelFontWeight: normal; +SymbolLineLabelTextDecoration: underline; +SymbolLineNumberColor: #a6a6a6; +SymbolLineNumberFontStyle: normal; +SymbolLineNumberFontWeight: normal; +SymbolLineNumberTextDecoration: underline; +SymbolLiteralBooleanColor: #b877ce; +SymbolLiteralBooleanFontStyle: normal; +SymbolLiteralBooleanFontWeight: normal; +SymbolLiteralBooleanTextDecoration: none; +SymbolLiteralDateColor: #b877ce; +SymbolLiteralDateFontStyle: normal; +SymbolLiteralDateFontWeight: normal; +SymbolLiteralDateTextDecoration: none; +SymbolLiteralEmptyColor: #b877ce; +SymbolLiteralEmptyFontStyle: normal; +SymbolLiteralEmptyFontWeight: normal; +SymbolLiteralEmptyTextDecoration: none; +SymbolLiteralNothingColor: #b877ce; +SymbolLiteralNothingFontStyle: normal; +SymbolLiteralNothingFontWeight: normal; +SymbolLiteralNothingTextDecoration: none; +SymbolLiteralNullColor: #b877ce; +SymbolLiteralNullFontStyle: normal; +SymbolLiteralNullFontWeight: normal; +SymbolLiteralNullTextDecoration: none; +SymbolLiteralNumericColor: #457e12; +SymbolLiteralNumericFontStyle: normal; +SymbolLiteralNumericFontWeight: normal; +SymbolLiteralNumericTextDecoration: none; +SymbolLiteralStringColor: #679f1e; +SymbolLiteralStringFontStyle: normal; +SymbolLiteralStringFontWeight: normal; +SymbolLiteralStringTextDecoration: none; +SymbolMeColor: #a13838; +SymbolMeFontStyle: normal; +SymbolMeFontWeight: normal; +SymbolMeTextDecoration: none; +SymbolModuleColor: #89894d; +SymbolModuleFontStyle: normal; +SymbolModuleFontWeight: normal; +SymbolModuleTextDecoration: none; +SymbolMultiLineSeperatorColor: #74384c; +SymbolMultiLineSeperatorFontStyle: normal; +SymbolMultiLineSeperatorFontWeight: normal; +SymbolMultiLineSeperatorTextDecoration: none; +SymbolNamedArgumentColor: #74384c; +SymbolNamedArgumentFontStyle: normal; +SymbolNamedArgumentFontWeight: normal; +SymbolNamedArgumentTextDecoration: none; +SymbolNamedOperatorColor: #385ba9; +SymbolNamedOperatorFontStyle: normal; +SymbolNamedOperatorFontWeight: normal; +SymbolNamedOperatorTextDecoration: none; +SymbolOperatorColor: #80a1a5; +SymbolOperatorFontStyle: normal; +SymbolOperatorFontWeight: normal; +SymbolOperatorTextDecoration: none; +SymbolParamByRefColor: #9b79b3; +SymbolParamByRefFontStyle: normal; +SymbolParamByRefFontWeight: normal; +SymbolParamByRefTextDecoration: none; +SymbolParamByValColor: #8d5eaf; +SymbolParamByValFontStyle: normal; +SymbolParamByValFontWeight: normal; +SymbolParamByValTextDecoration: none; +SymbolPropertyGetColor: #864f0f; +SymbolPropertyGetFontStyle: normal; +SymbolPropertyGetFontWeight: normal; +SymbolPropertyGetTextDecoration: none; +SymbolPropertyLetColor: #864f0f; +SymbolPropertyLetFontStyle: normal; +SymbolPropertyLetFontWeight: normal; +SymbolPropertyLetTextDecoration: none; +SymbolPropertySetColor: #864f0f; +SymbolPropertySetFontStyle: normal; +SymbolPropertySetFontWeight: normal; +SymbolPropertySetTextDecoration: none; +SymbolReturnValueColor: #ee8391; +SymbolReturnValueFontStyle: normal; +SymbolReturnValueFontWeight: normal; +SymbolReturnValueTextDecoration: none; +SymbolSubColor: #cf9a5d; +SymbolSubFontStyle: normal; +SymbolSubFontWeight: normal; +SymbolSubTextDecoration: none; +SymbolUDTColor: #a5630d; +SymbolUDTFontStyle: normal; +SymbolUDTFontWeight: normal; +SymbolUDTTextDecoration: none; +SymbolVariableColor: #939000; +SymbolVariableFontStyle: normal; +SymbolVariableFontWeight: normal; +SymbolVariableTextDecoration: none; +SymbolVariableUndeclaredColor: #b9929c; +SymbolVariableUndeclaredFontStyle: normal; +SymbolVariableUndeclaredFontWeight: normal; +SymbolVariableUndeclaredTextDecoration: none; +SystemIconsFullScreenFilter: none; +TabArrowsBackColor: #f2f2f2; +TabArrowsForeColor: black; +TabHoverBackColor: #f4f7fa; +TabIconFilter: none; +TabItemSelectedTextColor: rgb(10, 27, 10); +TabsBackColor: #e0e4e6; +TabsBorderBottom: 1px solid #a9a9a9; +TabDirtyMarker: radial-gradient(ellipse at center, rgb(170, 170, 170) 0%,rgb(170, 170, 170) 30%, transparent 35%); +TabSelectedBackColor: #ffffff; +TabSelectedHighlightBorderLeft: 1px solid rgb(221, 212, 212); +TabSelectedHighlightBorderRight: 1px solid rgb(221, 212, 212); +TabSelectedHighlightBorderTop: 2px solid #007eff !important; +TabSelectedHoverBackColor: rgb(250, 250, 250); +TabUnselectedBackColor: #fbfbfb; +TabUnselectedHighlightBorderLeft: 1px solid #a6a6a6; +TabUnselectedHighlightBorderRight: 1px solid #a6a6a6; +TabUnselectedHighlightBorderTop: 1px solid #bbbbbb; +ToolbarBackground: #F0f0f0; +ToolbarDropDownBorder: 1px solid #bdbdbd; +ToolbarPreviewButtonColor: #81a8b4; +ToolWindowBodyForeColor: rgb(44, 44, 44); +TreeItemHoverBackColor: rgb(219 219 219); +TreeItemSelectedBackColor: #efefef; +TreeItemSelectedForeColor: white; +VariablesPanelHoverTooltipBackColor: rgb(255 255 255); +VariablesPanelVariableNameFontWeight: bold; +VariablesPanelVariableNameForeColor: #5400b9; +VirtualDocBackColor: white; +VirtualDocForeColor: black; +VirtualDocMarkdownBackColor: rgb(255, 255, 255); +VirtualDocMarkdownBorderColor: #c8cfd7; +VirtualDocMarkdownForeColor: rgb(29, 29, 29); +VirtualDocMarkdownHeaderColor: rgb(197, 197, 4); +WarningTreeNodeColor: rgb(139 118 29); + +/* These icon properties support CSS data URIs, e.g. Something = url("data:image;base64,BASE64DATAHERE"); */ +ToolboxIconPointer: ; +ToolboxIconPictureBox: ; +ToolboxIconLabel: ; +ToolboxIconTextBox: ; +ToolboxIconFrame: ; +ToolboxIconCommandButton: ; +ToolboxIconCheckBox: ; +ToolboxIconOptionButton: ; +ToolboxIconComboBox: ; +ToolboxIconListBox: ; +ToolboxIconHScrollBar: ; +ToolboxIconVScrollBar: ; +ToolboxIconTimer: ; +ToolboxIconDriveListBox: ; +ToolboxIconDirListBox: ; +ToolboxIconFileListBox: ; +ToolboxIconShape: ; +ToolboxIconLine: ; +ToolboxIconImage: ; +ToolboxIconCheckMark: ; +ToolboxIconQRCode: ; +ToolboxIconData: ; +ToolboxIconOLE: ; +ToolboxIconMultiFrame: ; +ToolboxIconWebView2PackageWebView2: ; +ToolboxIconCOMMONCONTROLSDTPicker: ; +ToolboxIconCOMMONCONTROLSImageList: ; +ToolboxIconCOMMONCONTROLSListView: ; +ToolboxIconCOMMONCONTROLSMonthView: ; +ToolboxIconCOMMONCONTROLSProgressBar: ; +ToolboxIconCOMMONCONTROLSSlider: ; +ToolboxIconCOMMONCONTROLSTreeView: ; +ToolboxIconCOMMONCONTROLSUpDown: ; diff --git a/builder/themes/README.txt b/builder/themes/README.txt new file mode 100644 index 00000000..e1b499ca --- /dev/null +++ b/builder/themes/README.txt @@ -0,0 +1,14 @@ +!!! DO NOT MODIFY Dark.theme OR Light.theme + +Instead, create your own theme file, e.g. 'MyTheme.theme' + +At the top of your theme file, you can use the 'inherits' statement to pull in the theme data from an existing theme, e.g. 'dark' or 'light'. After that, you add any customized properties underneath. The provided Dark and Light theme files can be used as a guide as to what properties are available (they contain ALL available properties). + +The values for each property are matched to CSS values, so for example a 'TextDecoration' property would match to the 'text-decoration' CSS property, allowing values such as 'underline', 'line-through', and 'none'. Further documentation will be provided in due course. + +For example, the following text uses everything from the 'light' theme, and customizes two properties - you can copy the below into your own .theme file to test it. + + +inherits: light; +SymbolClassTextDecoration: underline; +MenuBackColor: red; \ No newline at end of file From a4e5507e3f70ad9147d2f8d5f41e9f9871e825c1 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 27 May 2026 15:31:33 +0200 Subject: [PATCH 3/9] Phase 11 docs: add PLAN-11.md --- builder/PLAN-11.md | 1210 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1210 insertions(+) create mode 100644 builder/PLAN-11.md diff --git a/builder/PLAN-11.md b/builder/PLAN-11.md new file mode 100644 index 00000000..9703e323 --- /dev/null +++ b/builder/PLAN-11.md @@ -0,0 +1,1210 @@ +# PLAN-11: Phase 11 — PARITY UPDATE (output-changing follow-ups) + +The parity-update phase. Read this together with [PLAN.md](PLAN.md) +(architecture overview), [PLAN-10.md](PLAN-10.md) (the cutover that +unblocks Phase 11), and [FUTURE-WORK.md](FUTURE-WORK.md) §B (the +backlog of output-changing items routed here) and §D (the sequencing +notes captured during the Phase 9 → 10/11 split planning that this +plan expands). + +Phase 11 has one job: **land every output-changing FUTURE-WORK item +that the byte-vs-Jekyll discipline of Phases 3-9 had deferred**. +After Phase 10 retired `accepted-divergences.mjs` and the eight +`verify-phase{1..8}.mjs` harnesses, the cost of an intentional +divergence dropped to zero -- the new acceptance bar is the expanded +`scripts/check_links.mjs` integrity checker plus a manual spot of +the rendered chrome. Phase 11 walks the five remaining items in +[FUTURE-WORK.md §B](FUTURE-WORK.md) that are still routed here: + +| Item | Subject | Headline change | +|---|---|---| +| B2 | Shiki theme generated from upstream `.theme` source files | Drops the ~470-line Rouge-class indirection in [highlight.mjs](highlight.mjs); regenerates the syntax-highlight stylesheet from upstream and changes per-span class names from Rouge tokens (`k`, `s`, `mi`) to a colour-palette scheme (`c1`, `c2`, …). | +| B1 | Mermaid `.mmd` → `.svg` automation | Adds a `mermaid.mjs` preprocessor invoking `mmdc`; the source `.mmd` becomes the canonical input. | +| B5 | Server-side copy-code button | Injects ` +
...
+ + ``` + + The container shape matches what just-the-docs JS produces at + runtime; the JS hook for the click handler stays. + - `builder/assets/js/just-the-docs.js` retires the runtime DOM- + injection path (the `processCodeBlocks` function). The click + handler stays; the script is ~20 lines smaller. + +- **B10**: + - `_site/assets/js/search-data.json` is unaffected (Phase 6 + output still pretty-printed for direct fetch). + - `_site-offline/assets/js/search-data.js` is **minified**: + drops insignificant whitespace, ~2.8 MB → ~1.7 MB. The wrap + `window.SEARCH_DATA = {...};` stays intact. + +- **B11**: + - [offline.mjs](offline.mjs)'s `deriveOfflineJtdJs` function + drops the two regex patches and rewrites them as `acorn` AST + transformations. The patched `just-the-docs.js` output is + byte-comparable to the current regex output; the test for + success is `check.bat` clean + the offline tree's search still + works. + - One new dep: `acorn` (and possibly `acorn-walk`). + +--- + +## 3. Module split + +``` +builder/ + highlight.mjs -390 / +20. Drop SCOPE_TO_ROUGE_CLASS, + bestRougeClass, JS_BUILTINS, the + per-language quirk block at lines + 218-340, the CPP_LIKE_RE / CPP_TOKEN_RE + fallback. Keep the run-coalescing + (`append` / `flush` / `pendingNewlines`) + and the line-continuation absorption. + Emit color-palette classes via the new + highlight-theme.mjs interface. Also + injects the B5 copy-button container. + highlight-theme.mjs NEW ~60-80 lines. Parses .theme files, + builds the palette + class table, + returns the generated CSS string. + themes/ Vendored upstream .theme source files + (Light/Dark/Classic + upstream + README.txt). Landed alongside this + plan; see § 6.1 for the resolution + of the FUTURE-WORK §D3 investigation. + mermaid.mjs NEW ~80 lines. Pre-Phase-5 regen step + (B1). + offline.mjs -25 / +60. AST-based JTD patcher (B11); + minification step folded into + searchDataJs (B10). + template.mjs +1 / -1. Stylesheet link swap (B2). + book.mjs +1 / -1. Same swap for the PDF tree. + pdf.mjs +1 / -1. REQUIRED_CSS entry swap. + index.mjs +20. Orchestrator: invoke mermaid.mjs + before Phase 5; thread the highlight + theme through Phase 3's init. + assets/css/rouge.css DELETED. Replaced by the build-time + generated tb-highlight.css. + assets/js/just-the-docs.js -20 lines. Drop processCodeBlocks + (B5). + package.json +2 deps (@mermaid-js/mermaid-cli, acorn); + possibly +1 (acorn-walk). + PLAN.md Phase 11 → shipped; Build Phases table + updates. + PLAN-11.md (this file) + FUTURE-WORK.md B1, B2, B5, B10, B11 → shipped. + +scripts/ + extract_theme_colors.py DELETED (B2 commit). + themes/ DELETED (B2 commit -- regenerated + artifact, no longer needed). + check_links.mjs Possibly +5 lines: the duplicate-id + check (Phase 10 addition) may need to + allow-list the B5 copy-button SVG ids + if any collide. Inspect after B5 + lands. +``` + +The module count grows by two (`highlight-theme.mjs`, `mermaid.mjs`) +and loses one asset (`rouge.css`). Net `builder/` line delta across +all five PRs: estimated -300 lines (B2 dominates the reduction; +B1+B5+B11 add small ports of work the runtime currently did). + +--- + +## 4. Implementation order + +Per [FUTURE-WORK.md §D5](FUTURE-WORK.md), Phase 11 lands as **five +independent PRs**, not one combined cutover. B2 lands first +(largest change, sets the pattern); the other four are independent +and can land in any order. + +| PR | Item | Verifies by | +|---|---|---| +| 1 | B2 — Shiki theme from `.theme` source | `build.bat && check.bat` clean. Visual spot of a Reference/.md page: tB code block colours match the previous (Rouge-driven) palette within the rendered noise (the `.theme` source is the canonical truth on both sides; only the class-name layer changed). Generated `tb-highlight.css` exists and is referenced from the page ``. | +| 2 | B1 — Mermaid auto-regen | `build.bat` regenerates the SVG; second `build.bat` is a no-op (timestamp check). Manual: change the `.mmd` source by one character, rebuild, confirm the SVG byte-changed. Revert. | +| 3 | B5 — Copy-button SSR | `check.bat` clean (no new duplicate-ids, well-formed HTML). Manual: load a page, click the copy button, confirm it copies. Confirm `just-the-docs.js` shrunk. | +| 4 | B10 — Search-data minification | `check.bat` clean. Manual: load an offline page, run a search, confirm hits return correctly. Confirm `_site-offline/assets/js/search-data.js` is ~1.7 MB. | +| 5 | B11 — AST JTD patcher | `check.bat` clean. Manual: confirm offline-tree navigation still highlights the current page in the sidebar; confirm offline search still works. The patched `just-the-docs.js` output may differ byte-for-byte from the pre-B11 regex-patched output -- that's fine if both produce the same observable behaviour. | + +### PR policy + +One PR per row. Each PR contains: + +- The implementation commit(s) for the item. +- The PLAN-11 / FUTURE-WORK / PLAN.md updates marking that item + shipped. +- The cleanup commit(s) for any artifacts the item retires + (`extract_theme_colors.py` / `scripts/themes/` for B2; the + `processCodeBlocks` block in `just-the-docs.js` for B5; the + regex patches for B11). + +Each PR must produce a working build before merge. Hook +enforcement stays as Phase 10 set it: **no `--no-verify`**. + +The five PRs do not have a strict order beyond "B2 first". A +caller picking up Phase 11 mid-stream can land B1/B5/B10/B11 in +whatever order their reviewers prefer; the only cross-PR +dependency is between B2 and B5 (B5 modifies the same +`renderCodeBlock` function B2 rewrote, so doing them in reverse +order means rebasing B5 onto B2's reduced shape -- minor, but +worth noting). + +--- + +## 5. Per-item specifications + +### 5.1. B2 — Shiki theme generated from `.theme` source + +**Source**: [FUTURE-WORK.md §B2](FUTURE-WORK.md) (headline), §B2a +(output-mode investigation), §D1 / §D3 / §D4 (sequencing notes). +The Phase 9 → 10/11 split planning recorded the design intent; +this section turns it into a concrete commit sequence. + +**Background**. The current pipeline indirects twice between +upstream colour and rendered span: + +``` +.theme files (USERPROFILE/Desktop/...) + │ + │ scripts/extract_theme_colors.py + ▼ +docs/_sass/custom/_twinbasic-{light,dark}.scss + │ (then through the just-the-docs Sass pipeline) + ▼ +builder/assets/css/just-the-docs-combined.css ← shipped + AND +builder/assets/css/rouge.css ← shipped (Rouge classes) + │ + │ Dim ← class names emitted by + │ highlight.mjs's SCOPE_TO_ROUGE_CLASS + ▼ +rendered page +``` + +The Rouge-class layer existed because Rouge's class set is fixed +and `rouge.css` was the only stylesheet that styled them; matching +Rouge's per-language quirks (`nf` for tB functions but `nc` for +JS function-CALL identifiers containing an uppercase letter, etc.) +required the ~120 lines of overrides in +[highlight.mjs:218-340](highlight.mjs:218). Under Phase 11, the +indirection drops: + +``` +.theme files (location TBD per §6.1 investigation) + │ + │ builder/highlight-theme.mjs (parses Symbol* properties, + │ builds palette + class table) + ▼ +builder/_site/assets/css/tb-highlight.css ← generated at build time + │ (~1-2 KB; one rule per unique colour in the palette) + │ + │ Dim ← palette class + ▼ +rendered page +``` + +The class names are now colour-derived (`c1`, `c2`, …). The +Rouge per-language quirks no longer matter because the rendered +colour comes from the token's Shiki scope chain directly mapped +to a palette entry; whether Rouge would have called the same +token `k` or `kd` is irrelevant. + +**Commit sequence within PR 1** (the investigation step from +[FUTURE-WORK.md §D3](FUTURE-WORK.md) ran ahead of the PR -- the +three `.theme` files are vendored under +[`builder/themes/`](themes/), resolution recorded in §6.1): + +1. **highlight-theme.mjs port**. Implements the parser + palette + builder + CSS emitter. ~60-80 lines. Stand-alone testable: + reads `builder/themes/{Light,Dark,Classic}.theme`, returns + `{ palette, classForScope, cssText }`. The Rouge mapping in + `extract_theme_colors.py`'s `ROUGE_TO_SYMBOL` table is the + source-of-truth ordering for which Symbol → which palette + entry, but the palette emits colour-based class names not + Rouge tokens. + +2. **highlight.mjs rewrite**. Drops `SCOPE_TO_ROUGE_CLASS`, + `bestRougeClass`, `JS_BUILTINS`, all the per-language `if + (!isTb && ...)` blocks at lines 218-340, `CPP_LIKE_RE`, + `CPP_TOKEN_RE`, `IDENT_FALLBACK_RE`, `STORAGE_TYPE_BARE_RE`, + `NONTB_PUNCT_OPERATOR_RE`, `JS_BUILTINS`, `CONTAINER_TOP_LEVEL`, + `hasContainerParent`. Keeps `renderRougeStyleSpans` (renamed to + `renderThemedSpans`) since the run-coalescing + line-continuation + absorption + multi-line-comment merging logic stays useful + under the new class scheme. The `classForScope` lookup replaces + `bestRougeClass`. + +3. **Asset + template updates**. Delete + `builder/assets/css/rouge.css`. Add a Phase 3 substep that + writes `/assets/css/tb-highlight.css` (the generated + stylesheet from step 1). Update `template.mjs`'s `` + stylesheet list to swap the link target. Update + `book.mjs:538` and `pdf.mjs:35` similarly for the PDF tree. + +4. **Cleanup**. Delete `scripts/extract_theme_colors.py` and + `scripts/themes/twinbasic-{classic,dark,light}.css`. Delete + `docs/_sass/custom/_twinbasic-{light,dark}.scss` IF Phase 10 + commit 7 has already landed (the Jekyll source set deletion); + otherwise leave them for commit 7 to take. Update + `builder/assets/README.md` to drop the `rouge.css` row and add + a "Phase 11 reference" line pointing at this PLAN. + +**Verification** (per [FUTURE-WORK.md §D2](FUTURE-WORK.md)): no +verify harness. The acceptance gates are: + +- `build.bat` succeeds; the new `tb-highlight.css` lands in + `_site/assets/css/`. +- `check.bat` clean (all five integrity flags, both online and + offline trees). +- Visual spot of a tB code block: colours match the previous + Rouge-driven render within rendering-noise tolerance. The + source of truth on both sides is the same `.theme` data; only + the class-name layer changed. A pixel-level diff is not + required. +- The page `` references `tb-highlight.css`, not + `rouge.css`. `_site/assets/css/rouge.css` is absent. + +**Light / dark mode coordination**. The current Jekyll-built +just-the-docs-combined.css carries `.language-tb .highlight .X` +rules inside `html.dark-mode { ... }`. Under Phase 11 the +generated `tb-highlight.css` is emitted in two passes -- the +light palette at root, the dark palette under `html.dark-mode` +-- so the dark-mode toggle continues to flip the syntax highlight +in lockstep with the rest of the chrome. See [§6.3](#63-light--dark-coordination). + +### 5.2. B1 — Mermaid `.mmd` → `.svg` automation + +**Source**: [FUTURE-WORK.md §B1](FUTURE-WORK.md), [PLAN-3 §15](PLAN-3.md). + +**Background**. The site currently has one mermaid diagram: +[docs/assets/images/mmd/2eff33e25804bc6ae919f694439dccbf.mmd](../docs/assets/images/mmd/2eff33e25804bc6ae919f694439dccbf.mmd) +with a hand-exported SVG sibling. The hand-export is what Phase 5 +copies; the `.mmd` source is editorial-only. Under Phase 11 the +SVG becomes a build artifact regenerated from the source whenever +the source is newer (or the SVG is missing). + +**New module** `builder/mermaid.mjs` (~80 lines): + +```js +// Phase 11 mermaid preprocessor: regenerates assets/images/mmd/*.svg +// from the matching *.mmd source when the SVG is missing or stale. +// Pre-Phase-5 step; the SVGs are copied verbatim by write.mjs once +// they exist. +// +// Idempotent: a second build with no source changes is a no-op +// (mtime check). Requires @mermaid-js/mermaid-cli at devDependency +// scope; the binary is invoked via `npx --no-install mmdc ...`. +// +// On the current tree (one source) the cold-cache regen is ~200 ms; +// the warm-cache check is sub-millisecond. + +import { promises as fs } from "node:fs"; +import path from "node:path"; +import fg from "fast-glob"; +import { spawn } from "node:child_process"; + +export async function regenerateMermaid(srcRoot) { + const mmdRoot = path.join(srcRoot, "assets/images/mmd"); + const sources = await fg("*.mmd", { cwd: mmdRoot, absolute: true }); + for (const src of sources) { + const svg = src.replace(/\.mmd$/, ".svg"); + if (await upToDate(svg, src)) continue; + await invokeMmdc(src, svg); + } +} +``` + +**Wiring**: + +- Orchestrator calls `await regenerateMermaid(srcRoot)` once, + immediately before Phase 5's WRITE ONLINE. The output SVGs + show up in Phase 1's discovered `staticFiles[]` only on the + *next* build -- on the regen build they need a one-shot + re-scan after `regenerateMermaid` returns. Cleanest: invoke + the regen step from the *top* of the orchestrator (before + Phase 1's discover), so Phase 1 picks up the freshly-emitted + SVGs. + +**Dependency**: + +- `@mermaid-js/mermaid-cli` as a devDependency. Invoked via + `npx --no-install mmdc -i -o -b transparent`. The + binary is ~250 MB installed (puppeteer / chromium); justify the + install cost only when at least one `.mmd` source exists -- + early-out the regen call when `sources.length === 0`. + +**Verification**: no harness; `build.bat && check.bat` clean. +Manual: edit the existing source by adding a node, rebuild, +confirm the SVG byte-changed; revert. + +**Edge cases**: + +- Source newer than SVG but SVG hand-edited: the regen + overwrites. Document in the WIP.md update: SVGs under + `assets/images/mmd/` are build artifacts; edit the `.mmd` + source, not the SVG. +- `mmdc` not installed: the spawn fails; the orchestrator + catches and logs `mermaid: skipped (mmdc not installed; run + npm install in builder/)`. The build continues using the + existing on-disk SVG. Caller can opt out of B1 entirely by + declining to install the devDependency. +- CI: the GitHub Actions workflow needs `npm ci` to include + devDependencies (it does by default with `actions/setup-node@v4`). + No extra setup required. + +### 5.3. B5 — Server-side copy-code button + +**Source**: [FUTURE-WORK.md §B5](FUTURE-WORK.md), [PLAN-3 §15](PLAN-3.md). + +**Current**: every `
` block ships unwrapped; the just-the-docs
+client JS (`processCodeBlocks` in `just-the-docs.js`) walks the DOM
+on `load`, finds each `
`, wraps it in a `
`, +and prepends a ` +
+
...
+
+
+``` + +The `` references the inline SVG +sprite that `template.mjs`'s `svgSprites` already injects at the +top of ``. Both `bi-clipboard` and `bi-clipboard-check-fill` +need to be in the sprite set; check the current set and add the +fill-variant if it's not there (the JS swaps the icon on click). + +**Client-side change**: + +- `just-the-docs.js`: delete `processCodeBlocks` (the DOM-injection + function). Keep the click handler that swaps the icon + copies + to clipboard; rebind it to the now-prebuilt button selector. +- Net JS reduction: ~20 lines. + +**Verification**: + +- `check.bat` clean (no new duplicate-ids from the per-page + buttons — each `
` gets its own button with no shared
+  `id`; the click handler selects via class, not id).
+- Manual: load a page, click the button, confirm the clipboard
+  receives the code. Confirm the icon swaps to the check-mark
+  on success.
+- Manual: disable JS, reload, confirm the button is visible
+  (server-side render) but inert. Acceptable degradation.
+
+**Risk**: the just-the-docs runtime may target the button via a
+selector that depended on its DOM position. Inspect the click
+handler before deleting `processCodeBlocks` and re-bind to the
+pre-rendered shape. The JS hook is preserved; only the injection
+path retires.
+
+### 5.4. B10 — `search-data.js` minification
+
+**Source**: [FUTURE-WORK.md §B10](FUTURE-WORK.md), [PLAN-7 §13](PLAN-7.md).
+
+**Current** ([offline.mjs](offline.mjs)'s `searchDataJs` step):
+generates `_site-offline/assets/js/search-data.js` by wrapping the
+pretty-printed `search-data.json` content in
+`window.SEARCH_DATA = {...};`. The wrapper preserves the source
+JSON's whitespace; the file is ~2.8 MB.
+
+**Change**: re-stringify the parsed JSON with `JSON.stringify(data)`
+(no indent) before wrapping. Estimated size after minification:
+~1.7 MB.
+
+**Implementation**:
+
+```js
+// offline.mjs, inside the searchDataJs step:
+const jsonText = await fs.readFile(jsonPath, "utf8");
+const parsed = JSON.parse(jsonText);
+const minified = JSON.stringify(parsed);                  // no indent
+const wrapped = `window.SEARCH_DATA = ${minified};\n`;
+await fs.writeFile(jsWrapperPath, wrapped, "utf8");
+```
+
+The online tree's `search-data.json` stays pretty-printed (Phase
+6 unchanged); the offline tree's `search-data.js` is the new
+minification target. The Lunr consumer (`initSearch` in
+`just-the-docs.js`) reads `window.SEARCH_DATA` and doesn't care
+about formatting.
+
+**Verification**:
+
+- `check.bat` clean.
+- Manual: load `_site-offline/index.html`, type a search query,
+  confirm hits return.
+- `wc -c _site-offline/assets/js/search-data.js` shows ~1.7 MB
+  (vs ~2.8 MB before).
+
+**Note on PLAN-7 §13's wider concern**: the original FUTURE-WORK
+entry mentioned `_site-offline.zip` size pressure. There's no
+`_site-offline.zip` artifact on the current build (the offline
+tree is consumed directly by users who clone or download the
+site). The minification is still worthwhile -- ~1.1 MB shaved
+off the offline tree's footprint -- but the "zip size" framing
+in FUTURE-WORK is a forward-looking concern, not a current one.
+
+### 5.5. B11 — AST-based `just-the-docs.js` patching
+
+**Source**: [FUTURE-WORK.md §B11](FUTURE-WORK.md), [PLAN-7 §13](PLAN-7.md).
+
+**Current** ([offline.mjs](offline.mjs)'s `deriveOfflineJtdJs`):
+two regex patches against the bundled `just-the-docs.js`:
+
+1. `navLink()` body — replaces the upstream `document.location.pathname`
+   active-page test with a resolved `link.href` test (so the
+   sidebar's active-class lookup works under `file://`).
+2. `initSearch()` body — replaces the upstream
+   `XMLHttpRequest('search-data.json')` fetch with a read of
+   `window.SEARCH_DATA` (XHR to `file://` is browser-blocked).
+
+The regexes anchor on specific function signatures inside the
+upstream source. A cosmetic upstream edit (variable rename,
+whitespace change, etc.) breaks the regex match and the patch
+silently no-ops -- `deriveOfflineJtdJs` returns warning lines but
+the build continues.
+
+**Change**: rewrite the patches as `acorn` AST rewrites. Parse
+`just-the-docs.js` once; walk the AST; locate the `navLink` and
+`initSearch` function declarations by name; replace the relevant
+body fragments; serialise the modified AST back to JS. Survives
+upstream cosmetic edits because the AST walker matches structure,
+not text.
+
+**Dependency**: `acorn` (production dep). Possibly `acorn-walk`
+for the walker convenience. Both small (~150 KB combined).
+
+**Implementation sketch**:
+
+```js
+import * as acorn from "acorn";
+import * as walk from "acorn-walk";
+// Optional: a serialiser like astring, OR build the output by
+// in-place string slicing using the AST node's range info (start
+// / end offsets in the source). The string-slicing approach
+// produces a smaller diff vs upstream because non-patched regions
+// stay byte-identical.
+
+function patchJtdAst(source) {
+  const ast = acorn.parse(source, { ecmaVersion: 2022, ranges: true });
+  const edits = [];   // [{ start, end, replacement }, ...]
+  walk.simple(ast, {
+    FunctionDeclaration(node) {
+      if (node.id.name === "navLink") {
+        edits.push(replaceNavLinkBody(node, source));
+      } else if (node.id.name === "initSearch") {
+        edits.push(replaceInitSearchBody(node, source));
+      }
+    },
+  });
+  return applyEdits(source, edits);
+}
+```
+
+**The two replacements** stay the same shape as the current regex
+versions; only the locator changes from regex to AST. The
+specific body fragments are the existing `OFFLINE_NAV_LINK` /
+`OFFLINE_INIT_SEARCH` constants in `offline.mjs`. The serialiser
+should preserve upstream byte-formatting outside the patched
+regions, which the string-slice approach does naturally.
+
+**Verification**:
+
+- `check.bat` clean.
+- Manual: load `_site-offline/index.html`, click around the
+  sidebar; confirm the current page highlights. Type a search;
+  confirm results return. Both behaviours regression-test the
+  two patches.
+- Byte-diff the new patched `just-the-docs.js` against the
+  pre-Phase-11 regex-patched version. Acceptable shapes:
+  byte-identical (the string-slice approach preserved all
+  non-patched bytes), or differences confined to whitespace /
+  trailing-newline normalisation inside the patched regions.
+  Anything beyond that needs investigation -- the AST walker
+  may have rewritten more than intended.
+
+**Risk**: an `acorn` parse failure on the upstream
+`just-the-docs.js` (e.g. a future just-the-docs release that uses
+unsupported syntax). Mitigation: pass `ecmaVersion: "latest"`,
+catch parse errors, fall back to the regex patcher with a clear
+warning. The regex code stays in tree as the fallback for one
+release cycle, then drops in a follow-up.
+
+**Note**: the upstream just-the-docs version is pinned via
+`docs/Gemfile` (`gem "just-the-docs", "= 0.10.1"`). Under Phase 11
+post-cutover, the Gemfile is no longer the source of truth -- the
+captured-once asset in `builder/assets/js/just-the-docs.js` is.
+A bump to a new just-the-docs version is an explicit re-capture
+step (see `builder/assets/README.md`); the AST patcher reduces
+the manual effort each bump requires.
+
+---
+
+## 6. B2 design notes
+
+(Expanded notes for §5.1. Skip this section if you're not
+implementing B2.)
+
+### 6.1. Where the `.theme` source files live (resolved)
+
+**Resolution**: vendored in repo under
+[`builder/themes/`](themes/). The three files
+(`Light.theme`, `Dark.theme`, `Classic.theme`) plus the upstream
+`README.txt` were copied verbatim from the twinBASIC IDE BETA's
+themes directory and committed alongside this plan, ahead of B2's
+PR proper. The pre-Phase-11 `scripts/extract_theme_colors.py`
+read them from `%USERPROFILE%/Desktop/twinBASIC_IDE_BETA_982/themes/`
+-- a local-only path that couldn't survive the move to CI. The
+vendor-in-repo shape replaces it; B2's `highlight-theme.mjs`
+reads from `builder/themes/` directly.
+
+This matches the convention `builder/assets/README.md` already
+established for the just-the-docs / Rouge / Lunr assets: the
+canonical source is upstream, the working copy lives in tree
+alongside a documented refresh procedure. For the themes the
+upstream's own `README.txt` (vendored alongside) carries the
+"do not modify" guidance.
+
+**Refresh procedure** (when a new twinBASIC IDE BETA changes the
+syntax-highlight palette):
+
+1. Locate the new build's `themes/` directory (today: under the
+   IDE installer's footprint).
+2. Diff the new `Light.theme` / `Dark.theme` / `Classic.theme`
+   against `builder/themes/`. If no `Symbol*` properties moved,
+   carry the diff over verbatim. If renamed or new `Symbol*`
+   entries appear, update `highlight-theme.mjs`'s `ROUGE_TO_SYMBOL`-
+   equivalent table (the scope → Symbol mapping kept from the
+   retired `extract_theme_colors.py`) to cover them, then carry
+   the diff over.
+3. Run `cd docs && build.bat && check.bat`. Spot a tB code block
+   to confirm the new palette renders. Commit `builder/themes/`
+   + any `highlight-theme.mjs` table changes in one commit.
+
+**Open notes carried over from the investigation**:
+
+- **File-format invariant.** The vendored files exhibit the
+  property-line `Name: value;` shape and the `Symbol*` grouping
+  the retired Python parser depended on. The B2 parser stays
+  forward-compatible (silently skip unknown `Symbol*`
+  properties) rather than hard-failing on novel entries -- a
+  future IDE adding a new symbol shouldn't break the build
+  before the docs caller catches up. The flip-side bound (an
+  *expected* `Symbol` going missing) is caught by the test
+  suite's spot of one rule per palette class in the rendered
+  CSS output.
+- **Licence.** The vendored upstream `README.txt` carries no
+  explicit licence statement, but the twinBASIC IDE BETA ships
+  the same three files in its public installer; vendoring the
+  files to support documentation that links to the IDE itself
+  is consistent with their distribution intent. If the upstream
+  ever publishes a more restrictive licence, the fallback is
+  fetch-at-build-time (a small step in `index.mjs` that pulls
+  the release artifact instead of reading from `builder/themes/`).
+- **Version recording.** The vendored copy was taken from the
+  twinBASIC_IDE_BETA_982 build. When refreshing, prepend a
+  comment to `builder/themes/Light.theme` (or similar) noting
+  the source version. The IDE's own theme files don't carry a
+  version stamp internally; tracking it in a comment is the
+  least-invasive shape.
+
+### 6.2. Palette and class-name scheme
+
+**Goal**: minimise per-span HTML bytes while keeping the
+stylesheet small and the class names stable across builds.
+
+**Approach** (per [FUTURE-WORK.md §B2a](FUTURE-WORK.md)
+output-mode investigation): custom-transformer Shiki output with
+class names derived from unique theme colours.
+
+1. **Parse** all three theme files (Light, Dark, Classic) into
+   `{ symbol -> properties }` maps.
+2. **Collect** the set of unique `Color` values across Light and
+   Dark (the two palettes that drive runtime; Classic is light-
+   inspection-only). Each unique colour gets a stable two-char
+   class ID:
+   - Sort the unique colours deterministically (e.g. alphabetical
+     by hex value).
+   - Assign `c1`, `c2`, … in sort order. Stable across builds
+     because the sort key is deterministic.
+3. **Build** the `Symbol -> classId` map for each palette
+   (Light, Dark). Symbols whose `Color` is the same fold to the
+   same `classId` (which is what we want -- the source already
+   identifies same-coloured symbols as the same visual category).
+4. **Build** the `scope -> classId` table the renderer reads.
+   The current `ROUGE_TO_SYMBOL` table in
+   `scripts/extract_theme_colors.py` encodes which Symbol owns
+   which scope; carry that table over verbatim (~25 entries).
+   The renderer's lookup becomes: "scope → Symbol → classId".
+5. **Emit** the stylesheet:
+
+   ```css
+   /* Light palette (root) */
+   .c1 { color: #448a63; font-style: normal; ... }   /* SymbolComment */
+   .c2 { color: #ad8c98; ... }                       /* SymbolConditionalCompilationDirective */
+   .c3 { color: #385ba9; ... }                       /* SymbolKeyword */
+   ...
+
+   /* Dark palette (under html.dark-mode) */
+   html.dark-mode .c1 { color: #6ab886; ... }
+   html.dark-mode .c2 { color: #c7a4b1; ... }
+   ...
+   ```
+
+**Estimated size**: ~25 palette entries × ~40 bytes per rule × 2
+palettes ≈ 2 KB. The current `rouge.css` is 2.3 KB; net wash.
+
+**Why class names not inline styles**: per
+[FUTURE-WORK.md §B2a](FUTURE-WORK.md), inline styles add
+~31 bytes per span vs ~22 for class wrappers (``).
+The 837-page corpus has ~50 K syntax-highlighted spans; the per-
+span overhead matters at scale. The class-based approach also
+keeps the dark-mode toggle working (inline styles would need CSS-
+variables-with-fallback, which ``
+shape doubles per-span overhead -- the worst option).
+
+### 6.3. Light / dark coordination
+
+**Current** (Jekyll-built `just-the-docs-combined.css`): the dark
+palette lives inside `html.dark-mode { ... }`. The `theme-switch.js`
+toggle flips that class on `` and every dark rule activates.
+
+**Under Phase 11**: same pattern. The generated `tb-highlight.css`
+emits the light palette as root rules and the dark palette inside
+`html.dark-mode`. The toggle JS is unchanged; the dark-mode flip
+continues to work in lockstep with the rest of the chrome.
+
+For the PDF tree: `_site-pdf/book.html` ships with the print
+stylesheet only (no dark mode). The PDF version of `tb-highlight.css`
+strips the `html.dark-mode` rules (or the writer chooses not to
+emit them for the PDF tree). Minor: ~50 bytes saved in the PDF
+artifact.
+
+### 6.4. The line-continuation / multi-line-comment carve-outs
+
+**Keep**. The current
+[highlight.mjs](highlight.mjs):130-380 logic that absorbs the
+leading whitespace of the line after `_` into the same `lc` span,
+and that merges adjacent block-comment spans across newlines into
+one multi-line span, both reflect the visual intent of the
+rendered code -- a multi-line comment looks better as one
+contiguous coloured block than as N per-line fragments separated
+by uncoloured newlines. Phase 11 keeps the run-coalescing machinery
+even though the Rouge-class-name shape (`lc`, `cm`) is gone; the
+new palette-class-name shape preserves the same merge invariants
+(adjacent runs with the same class merge; line-continuation
+absorbs the next line's leading whitespace).
+
+The carve-outs are about **rendered visual grouping**, not about
+**class-name semantics**, and they survive the class-naming
+change.
+
+### 6.5. Edge cases
+
+- **Empty / unrecognised Symbol** in the theme file. Fall back
+  to the parent scope or to `c0` (the default text colour --
+  derived from the theme's `TextColor` or equivalent). The
+  current Python script omits empty values; the JS port should
+  do the same.
+- **A code fence with an unknown language**. The current
+  highlight.mjs's `wrapperLang = lang ? lang.trim().toLowerCase() : "plaintext"`
+  shape stays. Tokens get `null` class (no `` wrap); they
+  inherit the default text colour from the `.highlight` rule.
+  No regression vs the current shape.
+- **A theme file that's missing entirely** (the location TBD per
+  §6.1 hasn't resolved at build time). Hard-fail with a clear
+  message; the build can't produce a syntax-highlight stylesheet
+  without it. No silent fall-back to a generic palette -- if the
+  upstream goes missing, the operator should know.
+
+---
+
+## 7. Design decisions and assumptions
+
+### 7.1. Decision record
+
+| ID | Decision | Why |
+|---|---|---|
+| D1 | Phase 11 lands as **five independent PRs**, not one combined cutover | Per [FUTURE-WORK.md §D5](FUTURE-WORK.md). B2 is large enough to deserve its own review window. The smaller items (B1, B5, B10, B11) are clean independent commits whose per-item revert path stays simple when bundled separately. |
+| D2 | `scripts/extract_theme_colors.py` and `scripts/themes/*.css` delete in **B2's PR**, not as a separate cleanup commit | Per [FUTURE-WORK.md §D4](FUTURE-WORK.md). The script exists only to feed the Rouge-class indirection in highlight.mjs that B2 retires; without B2's `SCOPE_TO_ROUGE_CLASS` consumer it has no caller. Same commit, same revert boundary. |
+| D3 | The `.theme` source files are **vendored under [`builder/themes/`](themes/)** rather than fetched at build time | The investigation step from [FUTURE-WORK.md §D3](FUTURE-WORK.md) resolved to "vendor in repo" (option 2.a in the original question set) before B2's PR opens. The vendored shape matches the convention `builder/assets/README.md` established for the just-the-docs / Rouge / Lunr assets, keeps the build deterministic (no network call), and survives a future restrictive-licence fall-back to fetch-at-build-time without a re-plan. §6.1 records the resolution and refresh procedure. |
+| D4 | **No Phase 11 verify harness** | Per [FUTURE-WORK.md §D2](FUTURE-WORK.md). Phase 10's expanded `check_links.mjs` (HTML well-formedness, duplicate-id, anchor resolution, sitemap / search completeness) is the regression detector. Adding a per-PR harness would duplicate effort; manual smoke-tests + `check.bat` are sufficient at this scale. |
+| D5 | B2 generates **palette-class names** (`c1`, `c2`, …), not inline styles or CSS variables | Per [FUTURE-WORK.md §B2a](FUTURE-WORK.md). Inline styles add ~31 bytes per span vs ~22 for class wrappers; CSS variables add ~60+. The 837-page corpus has ~50 K syntax-highlighted spans -- per-span overhead is the dominant cost. |
+| D6 | B2 generates the stylesheet **at build time** under `_site/assets/css/tb-highlight.css`, not vendored as a pre-extracted artifact | The build-time path keeps the upstream `.theme` data as the single source of truth: edit the `.theme` source, rebuild, the new colours flow through. A vendored stylesheet would require a manual two-step refresh (edit `.theme`, re-run an extractor) that's exactly what B2 is trying to retire. |
+| D7 | B1's mermaid regen runs **before Phase 1**, so the generated SVGs land in `staticFiles[]` naturally | The alternative (regen between Phase 1 and Phase 5) requires re-scanning the static-files set after regen. Running the preprocessor before discover folds the new SVGs into Phase 1's normal sweep. |
+| D8 | B1 depends on `@mermaid-js/mermaid-cli` as a **devDependency**, invoked via `npx --no-install mmdc` | Build-time only; runtime users don't need it. The `--no-install` flag avoids npx's silent auto-install behaviour. A missing `mmdc` is a hard fail with a clear message; the operator opts in by running `npm install` in `builder/`. |
+| D9 | B5's copy-button container is rendered by `highlight.mjs`, not by `template.mjs` | The container wraps individual code blocks, not the whole page. The natural home is the function that emits the code block. `template.mjs` stays page-level. |
+| D10 | B5 keeps the just-the-docs click-handler intact; only the DOM-injection path retires | The click-to-copy interaction is unchanged; only the moment of button creation moves from runtime to build-time. Minimal surface area to debug if a regression shows up. |
+| D11 | B10 minifies only the **offline** `search-data.js`, not the **online** `search-data.json` | The online JSON is fetched by Lunr at runtime and could-be-minified, but two consumer paths use it (the search runtime and any human-readable use-case for inspecting search hits). The offline `.js` is a write-once wrapper for `window.SEARCH_DATA` and has no other consumer; minification there is risk-free. |
+| D12 | B11's AST rewrites use **string-slice serialisation** (preserve upstream byte-formatting outside patched regions) rather than full re-serialisation via astring or similar | Keeps the diff vs upstream small and reviewable. A full re-serialisation would normalise whitespace site-wide and inflate the diff to "every line changed", obscuring whether the patch did anything beyond the two intended sites. |
+| D13 | B11 keeps the regex patcher in tree as a **fallback** for one release cycle, then drops it | Defence in depth against an acorn parse failure on a future just-the-docs version with unsupported syntax. The fallback fires only when acorn throws; once the release cycle is up, drop the fallback and tighten the dep on acorn's stability. |
+| D14 | Phase 11 does **not** touch `docs/_config.yml` | Same as Phase 10's [§7.D8](PLAN-10.md). The remaining Jekyll-only config keys are harmless ballast; cleaning them up is a separate task once the Phase 10 follow-up commit lands. |
+| D15 | `docs/_sass/custom/_twinbasic-{light,dark}.scss` deletes **in B2's PR if Phase 10 commit 7 has landed**, otherwise it stays for commit 7 to take | Avoids the bookkeeping question of "who owns these files now"; B2 retires their generator (`extract_theme_colors.py`), commit 7 retires the Jekyll source set including these SCSS partials. Whichever lands first claims the deletion. |
+
+### 7.2. Why no Phase 11 verify harness
+
+Phase 10 expanded `scripts/check_links.mjs` with five integrity
+check categories:
+
+- `--check-html` (HTML well-formedness)
+- `--check-a11y` (accessibility basics: missing alt, empty links)
+- `--check-ids` (duplicate `id="..."`)
+- `--check-sitemap` (sitemap.xml completeness)
+- `--check-search` (search-data.json completeness)
+
+`check.bat` runs all five against both `_site/` and
+`_site-offline/`. Every Phase 11 change is covered:
+
+- **B2**: changes per-span class names. The class-attribute
+  syntax stays well-formed; no new ids; the stylesheet link
+  resolves; the sitemap and search index are unaffected.
+  `--check-html` / `--check-a11y` / `--check-ids` catch any
+  malformed output.
+- **B1**: generates SVG files. The SVG well-formedness is
+  outside the link checker's scope, but the references to them
+  in HTML are checked (existing link-check).
+- **B5**: adds buttons and a container around code blocks.
+  `--check-html` catches mismatched-tag bugs; `--check-a11y`
+  catches the `aria-label` if it's missing (the spec calls for
+  one).
+- **B10**: changes a JS file's formatting. The link checker
+  doesn't parse JS; manual smoke (offline search works) is
+  the regression detector.
+- **B11**: same as B10 -- a JS-formatting change. Manual smoke
+  (offline navigation highlight + offline search) is the
+  detector.
+
+The Phase 11 acceptance bar is `check.bat` clean + the per-item
+manual smoke from §5.1-§5.5. No per-PR harness needed.
+
+### 7.3. Scope guardrails
+
+The line between Phase 11 and out-of-scope items is the criterion
+stated in [§intro](#plan-11-phase-11--parity-update-output-changing-follow-ups):
+the change is one of the five items routed to Phase 11 in
+[FUTURE-WORK.md §B](FUTURE-WORK.md). Implementer test for "is
+this Phase 11 or out of scope?":
+
+1. Check FUTURE-WORK.md §B for the candidate. If it's routed to
+   Phase 11 (B1, B2, B5, B10, B11) → Phase 11.
+2. If it's routed to "drop" (B6, B18) → out of scope; don't
+   reopen.
+3. If it's not in FUTURE-WORK.md at all → file a new entry
+   first; routing decision comes before implementation.
+
+The five items aren't a frozen set -- a Phase 12 (or a later
+Phase 11.5) can be filed if new output-changing follow-ups
+emerge. But mid-Phase-11 scope creep is the failure mode this
+guardrail prevents.
+
+---
+
+## 8. What's NOT in Phase 11
+
+These belong to Phase 12+ or are out of scope.
+
+### 8.1. Out of scope by routing
+
+- **B6 Linkify exception list** — dropped to "Re-add the entry
+  if a content shift makes bare URLs common in body prose"
+  ([FUTURE-WORK.md §B6](FUTURE-WORK.md)). Do not reopen.
+- **B18 Streaming write of `book.html`** — dropped because the
+  current scale (~5 MB) is two orders of magnitude below the
+  problem threshold ([FUTURE-WORK.md §B18](FUTURE-WORK.md)).
+  Do not reopen.
+
+### 8.2. Out of scope by topic
+
+- **Watch-mode / incremental rebuild.** Same as Phase 10's
+  [§7.D11](PLAN-10.md). Out of scope for Phase 11; if it ever
+  lands, it's a phase of its own.
+- **`docs/_config.yml` config-key cleanup.** The remaining
+  Jekyll-only keys are harmless ballast (see Phase 10 §5.8). If
+  Phase 10's follow-up commit (the Jekyll source set deletion)
+  hasn't landed when Phase 11 finishes, the config-cleanup pass
+  is still owed to that commit, not to Phase 11.
+- **The `one-offs/` directory.** Phase 9 §8.4 and Phase 10 §8.2
+  both ruled this out of scope; Phase 11 doesn't reopen it.
+- **A new build phase.** Phase 11 is feature work landing in the
+  existing modules; the eight-phase orchestrator shape doesn't
+  change. B1's mermaid step is a pre-phase preprocessor, not a
+  new phase.
+
+### 8.3. Things Phase 11 is **not** allowed to slip in
+
+- **`builder/assets/` re-extraction** triggered by a `just-the-docs`
+  gem version bump. The version is pinned at 0.10.1 via
+  `docs/Gemfile`; bumps are a separate manual procedure (see
+  `builder/assets/README.md`). B11 makes future bumps easier to
+  re-patch but doesn't perform a bump itself.
+- **markdown-it plugin updates.** Phase 3's plugin stack is
+  stable; any update risks per-page output drift that the new
+  integrity checker may or may not catch. Out of scope.
+- **Theme changes** beyond the syntax-highlight repaint. The
+  just-the-docs chrome (colours, spacing, typography) is
+  unchanged. B2 only affects the per-span colouring inside
+  `
` blocks.
+
+---
+
+## 9. Verification
+
+Per [FUTURE-WORK.md §D2](FUTURE-WORK.md), no Phase 11 verify
+harness. The acceptance gates per PR:
+
+### 9.1. Per-PR (gate before merge)
+
+1. `cd docs && build.bat` succeeds; `_site/`, `_site-offline/`,
+   `_site-pdf/` all produced.
+2. `cd docs && check.bat` clean (zero broken links, zero forbidden-
+   prefix hits, all five integrity flags PASS).
+3. Per-item manual smoke (from §5.1-§5.5):
+   - B2: visual spot of a tB code block; head link references
+     `tb-highlight.css`.
+   - B1: edit `.mmd` source, rebuild, SVG byte-changes; revert
+     and rebuild, no spurious regen.
+   - B5: click the copy button on a page; clipboard receives
+     the code.
+   - B10: offline search returns results; `search-data.js` is
+     ~1.7 MB.
+   - B11: offline navigation highlights; offline search works.
+
+### 9.2. Post-deploy smoke (per PR after merge)
+
+1. `https://docs.twinbasic.com/` loads.
+2. A Reference page with a tB code block renders the syntax
+   highlight (the colour palette is visually equivalent to the
+   pre-B2 render).
+3. The mermaid diagram on `docs/Documentation/Architecture/` (or
+   wherever its single source is referenced) renders.
+4. The copy button on a code block copies.
+5. The site search returns hits.
+6. The PDF book renders via `book.bat` (the post-deploy run
+   that exercises Phase 8 end-to-end).
+
+### 9.3. Cumulative (after all five PRs land)
+
+1. `_site/assets/css/rouge.css` is absent.
+2. `_site/assets/css/tb-highlight.css` is present and styles the
+   syntax highlight.
+3. `scripts/extract_theme_colors.py` and `scripts/themes/` are
+   gone.
+4. `builder/highlight.mjs` is ~80 lines (down from ~470).
+5. `_site-offline/assets/js/search-data.js` is ~1.7 MB (down
+   from ~2.8 MB).
+6. `builder/assets/js/just-the-docs.js` is ~20 lines shorter
+   (the `processCodeBlocks` retirement).
+7. `docs/assets/images/mmd/.svg` is byte-stable across
+   consecutive builds with no source change.
+
+---
+
+## 10. Dependencies
+
+Phase 11 adds two production deps and one devDependency:
+
+```json
+{
+  "dependencies": {
+    "fast-glob": "^3.3",
+    "gray-matter": "^4.0",
+    "js-yaml": "^4.1",
+    "markdown-it": "^14.0",
+    "markdown-it-attrs": "^4.3",
+    "markdown-it-deflist": "^3.0",
+    "markdown-it-footnote": "^4.0",
+    "shiki": "^1.0",
+    "acorn": "^8.0",                     // NEW (B11)
+    "acorn-walk": "^8.0"                 // NEW (B11, optional convenience)
+  },
+  "devDependencies": {
+    "@mermaid-js/mermaid-cli": "^11.0"   // NEW (B1)
+  }
+}
+```
+
+- **acorn** + **acorn-walk** (B11): ~150 KB combined; production
+  dep because they're invoked during the offline-tree build.
+  Stable API; small surface.
+- **@mermaid-js/mermaid-cli** (B1): ~250 MB installed (pulls
+  puppeteer / chromium). devDependency so production deployments
+  without a `.mmd` source don't pay the install cost. CI installs
+  via `npm ci` by default.
+
+`shiki` stays unchanged (the existing Shiki + TextMate-grammar
+pipeline keeps working; B2 only changes what the per-token
+classifier returns).
+
+No removals. The seven existing deps + two new are the Phase 11
+end-state.
+
+---
+
+## 11. File layout after Phase 11
+
+```
+/
+  builder/
+    PLAN.md                       (updated: Phase 11 → shipped)
+    PLAN-1.md ... PLAN-10.md      (unchanged)
+    PLAN-11.md                    (this file)
+    FUTURE-WORK.md                (B1, B2, B5, B10, B11 marked shipped)
+    README.md                     (the existing module map updated for
+                                   the new modules and the rouge.css
+                                   removal)
+    index.mjs                     (+ 20 lines: mermaid pre-step,
+                                   highlight-theme init)
+    highlight.mjs                 (~ 80 lines, down from ~ 470)
+    highlight-theme.mjs           NEW (~ 60-80 lines)
+    mermaid.mjs                   NEW (~ 80 lines)
+    offline.mjs                   (B10 minify + B11 AST patcher;
+                                   ~+ 35 lines net)
+    template.mjs                  (one-line stylesheet swap)
+    book.mjs                      (one-line stylesheet swap)
+    pdf.mjs                       (one-line REQUIRED_CSS swap)
+    package.json                  (+ acorn, acorn-walk, mermaid-cli)
+    themes/                       Vendored per § 6.1 (Light.theme,
+                                   Dark.theme, Classic.theme,
+                                   upstream README.txt). Landed
+                                   alongside PLAN-11; B2's PR
+                                   reads from here.
+    one-offs/                     (unchanged)
+    assets/
+      css/
+        rouge.css                 DELETED (B2)
+        (other entries unchanged)
+      js/
+        just-the-docs.js          (- 20 lines, B5)
+        (other entries unchanged)
+      README.md                   (rouge.css row dropped; PLAN-11
+                                   reference added)
+
+  docs/
+    build.bat                     (unchanged from Phase 10)
+    check.bat                     (unchanged from Phase 10)
+    serve.bat                     (unchanged from Phase 10)
+    WIP.md                        (the "Build pipeline" section gets
+                                   a Phase 11 paragraph noting the
+                                   five PRs and pointing at PLAN-11)
+    _plugins/                     (Phase 10 status: still present
+                                   pending commit 7; Phase 11 doesn't
+                                   touch)
+    _includes/                    (same)
+    _layouts/                     (same)
+    _sass/                        (same -- if commit 7 has not landed;
+                                   B2 deletes _sass/custom/_twinbasic-*
+                                   IF commit 7 has landed, otherwise
+                                   commit 7 takes them)
+    Gemfile / Gemfile.lock        (same)
+    _config.yml                   (unchanged)
+    assets/
+      images/
+        mmd/
+          .mmd              (canonical source; unchanged)
+          .svg              (regenerated artifact; byte-stable
+                                   across builds modulo source edits)
+
+  scripts/
+    check_links.mjs               (Phase 10 form; possibly + 5 lines
+                                   for B5 copy-button id allow-list
+                                   IF needed -- inspect after B5
+                                   lands)
+    convert_em_dash_separators.py (unchanged)
+    extract_theme_colors.py       DELETED (B2)
+    themes/                       DELETED (B2 -- regenerated artifact)
+
+  .github/workflows/
+    pages.yml                     (or jekyll-gh-pages.yml --
+                                   unchanged from Phase 10)
+```
+
+After Phase 11 lands, the only remaining Phase 10 follow-up is
+the deletion of the Jekyll source set (`docs/_plugins/`,
+`docs/_includes/`, `docs/_layouts/`, `docs/_sass/`, `Gemfile`,
+`Gemfile.lock`, `docs/_profile/`) per PLAN-10 §5.8. That commit
+is sequenced two weeks after the Phase 10 cutover and is
+independent of Phase 11 -- it may have landed before, during, or
+after Phase 11 depending on calendar.
+
+---
+
+## 12. What "done" Phase 11 enables
+
+After all five PRs land:
+
+- **The syntax-highlight palette stays in sync with upstream
+  automatically.** Edit the `.theme` source (or refresh the
+  vendored copy per the procedure in §6.1), rebuild, the new
+  colours flow through end-to-end. No manual re-mapping through
+  Rouge classes; no `extract_theme_colors.py` to re-run.
+- **Mermaid diagrams update from source.** Edit a `.mmd`,
+  rebuild, the SVG regenerates. No Typora export step.
+- **The code-block copy button is server-rendered.** Visible
+  before JS loads; no run-time DOM mutation; the just-the-docs
+  client bundle shrinks marginally.
+- **The offline tree's biggest asset shrinks.** ~1.1 MB
+  recovered on `_site-offline/`.
+- **Future just-the-docs version bumps are easier to re-patch.**
+  The AST patcher survives cosmetic upstream edits the regex
+  patcher could miss.
+- **`builder/highlight.mjs` is ~80 lines.** The ~390 lines of
+  per-language Rouge-quirk overrides are gone. Reading the
+  module to understand what it does no longer requires reading
+  six TextMate-scope-to-Rouge-token decision tables.
+
+The five items shipped together close out the Phase-10-deferred
+backlog from FUTURE-WORK.md §B. After Phase 11, the only open
+follow-up in FUTURE-WORK.md is the A1 investigation
+(hidden-secondary-divergences) -- which is now a content
+question, not a code question, and has no obvious next phase
+to belong to.
+
+The cutover phase, parity-update phase, and original eight build
+phases are then complete; further work on the builder is
+incremental enhancement against a stable baseline.

From 784b777fca7c2c357a7f20a80edbb595f62611f8 Mon Sep 17 00:00:00 2001
From: Kuba Sunderland-Ober 
Date: Wed, 27 May 2026 16:00:56 +0200
Subject: [PATCH 4/9] Phase 11 B2: Shiki theme generated from .theme source

Drop the Rouge-class indirection in builder/highlight.mjs. The new
builder/highlight-theme.mjs parses the vendored .theme files under
builder/themes/, derives a deterministic Symbol-keyed palette, and
emits _site/assets/css/tb-highlight.css at build time. Per-span
class names switch from Rouge tokens (k, s, mi) to a palette scheme
(c1, c2, ...); same-coloured symbols collapse to one classId (the
five Literal* symbols fold to one rule).

builder/highlight.mjs shrinks from ~470 lines to ~190 -- the
SCOPE_TO_ROUGE_CLASS table, the ~120 lines of per-language quirks,
JS_BUILTINS, the C/C++ token re-tokeniser, and the bestRougeClass
walker all delete. The run-coalescing, line-continuation absorb,
and block-comment merge logic stay; the lc/cm class checks now
resolve via theme.classForSymbol.

Asset wiring: writePhase takes a generatedAssets parameter, the
orchestrator threads highlighter.themeCss through it, and the file
lands at /assets/css/tb-highlight.css alongside the
verbatim-copied theme tree. template.mjs adds the  in the
head between just-the-docs-combined.css and just-the-docs-head-nav.css;
book.mjs and pdf.mjs swap rouge.css -> tb-highlight.css for the PDF
tree. scripts/extract_theme_colors.py, scripts/themes/*.css, and
builder/assets/css/rouge.css delete. The docs/_sass/custom/_twinbasic-*.scss
partials stay -- Phase 10 commit 7 (Jekyll source set deletion) will
take them when it lands.
---
 WIP.md                               |  10 +
 builder/FUTURE-WORK.md               |  45 ++-
 builder/PLAN-11.md                   |   2 +-
 builder/PLAN.md                      |  40 +--
 builder/assets/README.md             |  64 ++--
 builder/assets/css/rouge.css         | 116 --------
 builder/book.mjs                     |   2 +-
 builder/highlight-theme.mjs          | 221 ++++++++++++++
 builder/highlight.mjs                | 429 +++++----------------------
 builder/index.mjs                    |   5 +-
 builder/pdf.mjs                      |   2 +-
 builder/template.mjs                 |   1 +
 builder/write.mjs                    |  21 +-
 scripts/extract_theme_colors.py      | 246 ---------------
 scripts/themes/twinbasic-classic.css | 200 -------------
 scripts/themes/twinbasic-dark.css    | 200 -------------
 scripts/themes/twinbasic-light.css   | 200 -------------
 17 files changed, 418 insertions(+), 1386 deletions(-)
 delete mode 100644 builder/assets/css/rouge.css
 create mode 100644 builder/highlight-theme.mjs
 delete mode 100644 scripts/extract_theme_colors.py
 delete mode 100644 scripts/themes/twinbasic-classic.css
 delete mode 100644 scripts/themes/twinbasic-dark.css
 delete mode 100644 scripts/themes/twinbasic-light.css

diff --git a/WIP.md b/WIP.md
index 905ce53a..5553af0f 100644
--- a/WIP.md
+++ b/WIP.md
@@ -432,6 +432,16 @@ The site builds via [builder/](builder/), a custom Node.js static site generator
 
 The diff and verify harnesses (`_triage.mjs`, `_diff.mjs`, `_diff_all.mjs`, `_audit_accepted.mjs`, `_sitemap_diff.mjs`, `_spot.mjs`, `verify-phase{1..8}.mjs`, `accepted-divergences.mjs`) were retired in the Phase 10 cutover commit. Regression detection now relies on `scripts/check_links.mjs` (expanded into a site-integrity checker; see [docs/check.bat](docs/check.bat)).
 
+### Phase 11 — parity update
+
+Phase 11 (in progress, see [builder/PLAN-11.md](builder/PLAN-11.md)) lands the output-changing follow-ups that the Phase 3-9 byte-vs-Jekyll discipline had deferred. Five independent PRs:
+
+- **B2 — Shiki theme generated from `.theme` source.** Shipped. `scripts/extract_theme_colors.py` and `builder/assets/css/rouge.css` are gone; `builder/highlight-theme.mjs` parses the vendored `builder/themes/Light.theme` + `Dark.theme` files and emits `_site/assets/css/tb-highlight.css` at build time. Per-span class names switched from Rouge tokens (`k`, `s`, `mi`) to a palette scheme (`c1`, `c2`, …). `builder/highlight.mjs` shrank from ~470 lines to ~190 — the per-language Rouge-quirk overrides folded into the scope-to-Symbol table.
+- **B1 — Mermaid `.mmd` → `.svg` automation.** Pending.
+- **B5 — Server-side copy-code button.** Pending.
+- **B10 — `search-data.js` minification.** Pending.
+- **B11 — AST-based `just-the-docs.js` patching.** Pending.
+
 ### Historical note
 
 The site was originally built with Jekyll + just-the-docs. The Jekyll source set (`docs/_plugins/`, `docs/_includes/`, `docs/_layouts/`, `docs/_sass/`, `docs/Gemfile`) was retired in the Phase 10 cutover commit; the directories were kept for one release cycle as reference and then deleted in a follow-up cleanup commit. Search the git log for `Phase 10` to find both commits.
diff --git a/builder/FUTURE-WORK.md b/builder/FUTURE-WORK.md
index 61f12013..9653f885 100644
--- a/builder/FUTURE-WORK.md
+++ b/builder/FUTURE-WORK.md
@@ -146,30 +146,27 @@ close the loop so the source `.mmd` is the canonical input and the
 SVG regenerates automatically. Independent addition; doesn't touch
 any phase code.
 
-### B2. Switch to Shiki-themed output (PLAN-3 §15 / §D3)
-
-**Routing**: → **Phase 11** (headline item). Regresses HTML
-byte-match (per-span class names) AND the rouge.css file.
-
-**Approach**: rather than the original "switch to Shiki's default
-`` output", the Phase 11 plan generates the
-Shiki theme **from the upstream twinBASIC `.twin` style source
-files** during the build, replacing the current
-`scripts/extract_theme_colors.py` mapping that produces Rouge
-classes. The original styling information lives in the `.twin`
-files; the current pipeline indirects through Rouge classes because
-Rouge's class set is fixed. Reading the `.twin` source directly lets
-the syntax colors stay in sync with upstream without manual remap.
-
-**Trigger**: Phase 10 lands (the cutover removes the byte-vs-Jekyll
-acceptance bar).
-
-Phase 3 currently maps Shiki's TextMate scopes onto Rouge class
-names so the existing `assets/css/rouge.css` keeps working
-byte-for-byte. The Phase 11 change drops the mapper, generates Shiki
-styles directly from the `.twin` source files, and changes the
-per-span class names from Rouge tokens (`k`, `s`, `mi`) to a
-colour-palette scheme (`c1`, `c2`, … per unique theme colour).
+### B2. Switch to Shiki-themed output (PLAN-3 §15 / §D3) — **shipped in Phase 11**
+
+**Routing**: → **shipped in Phase 11** ([PLAN-11.md §5.1](PLAN-11.md)).
+Headline parity-update item.
+
+`scripts/extract_theme_colors.py` and `builder/assets/css/rouge.css`
+are gone; `builder/highlight-theme.mjs` parses the three vendored
+`.theme` files under `builder/themes/` (Light, Dark, Classic) and
+emits `_site/assets/css/tb-highlight.css` at build time. Per-span
+class names switched from Rouge tokens (`k`, `s`, `mi`) to a
+colour-palette scheme (`c1`, `c2`, …) — one classId per unique
+(Light props, Dark props) tuple, so symbols sharing both palettes'
+properties collapse to a single rule (the five Literal* symbols
+fold to one `c13` on the current themes). `builder/highlight.mjs`
+shrank from ~470 lines to ~190; the per-language Rouge-quirk
+overrides folded into the theme's scope-to-Symbol table.
+
+Light palette ships at root; dark palette nests under
+`html.dark-mode` so the chrome's theme toggle flips both halves
+together. The PDF tree links `tb-highlight.css` from `book.html`
+in place of the retired `rouge.css`.
 
 #### B2a. Shiki output-mode investigation (findings, 2026-Q2)
 
diff --git a/builder/PLAN-11.md b/builder/PLAN-11.md
index 9703e323..7a064abc 100644
--- a/builder/PLAN-11.md
+++ b/builder/PLAN-11.md
@@ -56,7 +56,7 @@ diagram on the current tree, ~200 ms cost when it runs); B5 and
 B11 are perf-neutral. Net build-wall-clock effect: neutral to
 slightly faster.
 
-## Status: planned
+## Status: in progress — B2 shipped; B1, B5, B10, B11 pending
 
 ---
 
diff --git a/builder/PLAN.md b/builder/PLAN.md
index fd467d1d..b6d7e2a1 100644
--- a/builder/PLAN.md
+++ b/builder/PLAN.md
@@ -43,13 +43,14 @@ heading-hierarchy checks). The Jekyll source set (`docs/_plugins/`,
 stays in tree for one release cycle as a reference, then deletes
 in a follow-up commit.
 
-**Phase 11** (planned, no PLAN-11.md yet) picks up the output-changing
+**Phase 11** ([PLAN-11.md](PLAN-11.md)) picks up the output-changing
 FUTURE-WORK items now that the byte-vs-Jekyll acceptance bar is
-gone: Shiki theming generated from upstream `.twin` source files
-(replacing the Rouge-class indirection in
-[highlight.mjs](highlight.mjs) and dropping `rouge.css`), mermaid
-auto-gen, copy-code SSR, search-data minification, AST-based JTD
-patcher. Sequenced after Phase 10 because Phase 11's intentional
+gone. Lands as five independent PRs: B2 (Shiki theming generated
+from the vendored `.theme` files, replacing the Rouge-class
+indirection in [highlight.mjs](highlight.mjs) and dropping
+`rouge.css`) — shipped; B1 (mermaid auto-gen), B5 (copy-code SSR),
+B10 (search-data minification), B11 (AST-based JTD patcher) —
+pending. Sequenced after Phase 10 because Phase 11's intentional
 divergences are only free to land once `accepted-divergences.mjs`
 stops being the acceptance bar.
 
@@ -141,8 +142,8 @@ Phase 6: AUXILIARIES     ~100ms   Redirects, sitemap, search-data.json, robots.t
 Phase 7: WRITE OFFLINE   ~1000ms  URL-rewritten copy to _site-offline/                        [shipped]
 Phase 8: WRITE PDF       ~150ms   Sparse copy to _site-pdf/                                   [shipped]
 Phase 9: QoL + DOCS      (-200ms) FUTURE-WORK consolidation; no output change                 [shipped]
-Phase 10: CUTOVER        (n/a)    Retire Jekyll; pivot harnesses to site-integrity checker    [planned]
-Phase 11: PARITY UPDATE  (TBD)    Output-changing FUTURE-WORK items (Shiki, mermaid, ...)     [planned]
+Phase 10: CUTOVER        (n/a)    Retire Jekyll; pivot harnesses to site-integrity checker    [shipped]
+Phase 11: PARITY UPDATE  (TBD)    Output-changing FUTURE-WORK items (Shiki, mermaid, ...)     [in progress — B2 shipped]
 ```
 
 Timings are wall-clock measurements from `node builder/index.mjs` on
@@ -410,19 +411,20 @@ heading-hierarchy checks. The Jekyll source set (`docs/_plugins/`,
 stays for one release cycle as reference, then deletes in a
 follow-up commit. Full spec: [PLAN-10.md](PLAN-10.md).
 
-### Phase 11: PARITY UPDATE (planned)
+### Phase 11: PARITY UPDATE (in progress)
 
 Output-changing FUTURE-WORK items the byte-parity-with-Jekyll
-discipline of Phases 3-9 had deferred. Headline item: **Shiki
-themes generated from upstream twinBASIC `.twin` source files**
-(replacing the `scripts/extract_theme_colors.py` Rouge-class
-mapping and dropping `rouge.css` entirely). Other candidates:
-mermaid `.mmd` auto-regeneration (B1), server-side copy-code
-button (B5), search-data minification (B10), AST-based JTD JS
-patcher (B11). B6 (linkify) and B18 (streaming book.html write)
-were dropped. No PLAN-11.md yet -- sequenced after Phase 10 lands
-because Phase 11's intentional byte divergences only become free to
-accept once the Jekyll-comparison harness is retired.
+discipline of Phases 3-9 had deferred. Headline item **B2 (shipped):
+Shiki themes generated from the vendored `.theme` files** —
+`scripts/extract_theme_colors.py` and `rouge.css` are gone;
+`builder/highlight-theme.mjs` builds the palette and emits
+`tb-highlight.css` at build time; `builder/highlight.mjs` shrank
+from ~470 lines to ~190 (the per-language Rouge-quirk overrides
+folded into the scope-to-Symbol table). Remaining items, each as
+its own PR: B1 (mermaid `.mmd` auto-regen), B5 (server-side copy-
+code button), B10 (search-data minification), B11 (AST-based JTD
+patcher). B6 (linkify) and B18 (streaming book.html write) were
+dropped. Full spec: [PLAN-11.md](PLAN-11.md).
 
 ## Static Asset Extraction (One-Time Setup)
 
diff --git a/builder/assets/README.md b/builder/assets/README.md
index b5d881bc..0f5c685c 100644
--- a/builder/assets/README.md
+++ b/builder/assets/README.md
@@ -15,16 +15,19 @@ into `_site-offline/` and re-run the offlinify rewrites itself.
 
 ## Inventory
 
-All seven files are Jekyll-build outputs, captured from
+The six files below are Jekyll-build outputs, captured from
 `docs/_site/assets/` after a clean `bundle exec jekyll build`. The
-upstream sources Jekyll consumed are listed for reference.
+upstream sources Jekyll consumed are listed for reference. The
+syntax-highlight stylesheet (`tb-highlight.css`) is a seventh asset
+that is **generated at build time** by `builder/highlight-theme.mjs`
+from the vendored `builder/themes/*.theme` source files — see
+[PLAN-11.md](../PLAN-11.md) §5.1 (B2).
 
 | File | Upstream source | Notes |
 |---|---|---|
 | `css/just-the-docs-combined.css` | `docs/assets/css/just-the-docs-combined.scss` — pulls in `_sass/just-the-docs.scss.liquid` (via the just-the-docs gem) plus `_sass/custom/twinbasic-light` / `_sass/custom/twinbasic-dark` for the project's dark/light variants and `_sass/custom/custom.scss` / `_sass/custom/admonitions.scss` for site-specific tweaks | The site stylesheet. Compiled by Jekyll's Sass pipeline; vendored here to avoid a Sass dep in the JS build. |
 | `css/just-the-docs-head-nav.css` | `docs/assets/css/just-the-docs-head-nav.css` — hand-written CSS with a small Liquid prelude (newline-capture) | Per-page nav-prefix override sheet. |
 | `css/print.css` | `docs/assets/css/print.css` — hand-written self-contained print stylesheet | The `@media print` sheet; used by Phase 8's PDF tree too. |
-| `css/rouge.css` | `docs/assets/css/rouge.css` — hand-written, the Rouge `github.light` palette mapped to `.k` / `.kc` / `.nb` / `.s` / … class names | The syntax-highlight scope-to-colour rules. Phase 3's Shiki driver emits the same Rouge / Pygments class names so this file styles both Rouge-rendered and Shiki-rendered code blocks. |
 | `js/just-the-docs.js` | just-the-docs gem `assets/js/just-the-docs.js` (version pinned by `docs/Gemfile`) | The runtime that wires the sidebar, search, copy-button, dark-mode toggle. |
 | `js/theme-switch.js` | `docs/assets/js/theme-switch.js` — project-local script | The dark-mode toggle. |
 | `js/vendor/lunr.min.js` | just-the-docs gem `assets/js/vendor/lunr.min.js` | The search runtime. |
@@ -50,17 +53,19 @@ cd docs && bundle exec jekyll build && cd ..
 cp docs/_site/assets/css/just-the-docs-combined.css   builder/assets/css/
 cp docs/_site/assets/css/just-the-docs-head-nav.css   builder/assets/css/
 cp docs/_site/assets/css/print.css                    builder/assets/css/
-cp docs/_site/assets/css/rouge.css                    builder/assets/css/
 cp docs/_site/assets/js/just-the-docs.js              builder/assets/js/
 cp docs/_site/assets/js/theme-switch.js               builder/assets/js/
 cp docs/_site/assets/js/vendor/lunr.min.js            builder/assets/js/vendor/
 ```
 
-Then re-run `node builder/verify-phase5.mjs`. A `diff -rq docs/_site
-docs/_site-new` "Files differ" entry for any of these seven paths
-means the bundled copy has drifted from Jekyll's current output —
-re-extracting closes the gap. (Per-page HTML divergences are
-unrelated; see PLAN-5.md §10.)
+Note: `rouge.css` was retired in PLAN-11 §5.1 (B2). The syntax-
+highlight stylesheet is now `tb-highlight.css`, generated at build
+time from `builder/themes/*.theme` — no extraction step.
+
+The Phase 5 verify harness retired in PLAN-10's cutover; regression
+detection now relies on `scripts/check_links.mjs`. After re-extracting,
+run `cd docs && build.bat && check.bat` and confirm the rendered
+chrome still works.
 
 ## CSS class contract
 
@@ -101,21 +106,26 @@ adjusting the JS emitter (or vice versa) breaks the rendered chrome.
   `octicon octicon-{alert,info,light-bulb,report,stop}`
   (the GitHub-flavoured admonition palette)
 
-**Syntax highlighting** (`highlight.mjs` + `rouge.css`):
-
-- The Rouge / Pygments token class set: `.k` keyword, `.kc`
-  keyword-constant, `.nb` name-builtin, `.s` string, `.c` comment,
-  `.n` name, `.o` operator, …
-- Phase 3's Shiki driver emits these class names from
-  `twinbasic.tmLanguage.json` scope mappings, so `rouge.css` styles
-  them with no extra translation step.
-- Phase 3 also emits `` (line continuation) and
-  `` (string escape) for tB-specific tokens; both
-  inherit Rouge's class palette.
-
-If `rouge.css` ever stops covering a class name the highlighter
-emits, the affected tokens render as unstyled text — visible in the
-rendered site as black-on-white code spans inside the otherwise-
-coloured block. The fix is either to add the class to `rouge.css`
-(if the source omitted it) or to remap the Shiki scope in
-`highlight.mjs` to a name `rouge.css` already covers.
+**Syntax highlighting** (`highlight.mjs` + `highlight-theme.mjs` + the
+generated `tb-highlight.css`):
+
+- The palette-class set: `.c1`, `.c2`, … `.cN` — one class per
+  unique (Light props, Dark props) tuple across the Symbols the
+  renderer can land on. Stable across builds because the tuples
+  sort deterministically.
+- `highlight.mjs` looks up each token's Shiki scope chain via the
+  theme's `classForScope`, then emits `` for hits
+  and no wrap for misses (plain punctuation, generic identifiers,
+  HTML tag names — the IDE theme doesn't colour those).
+- `tb-highlight.css` is generated at build time and written to
+  `/assets/css/tb-highlight.css`; the light palette lives
+  at root, the dark palette under `html.dark-mode`. The chrome's
+  theme toggle flips both halves together.
+
+If the IDE refreshes its themes (a tB BETA bump that changes
+palette colours or adds new Symbols), refresh the three files under
+`builder/themes/` from the BETA's installer — `Light.theme`,
+`Dark.theme`, `Classic.theme` — and rebuild. `highlight-theme.mjs`'s
+`SCOPE_TO_SYMBOL` table maps Shiki scopes to the upstream Symbol
+names; only changes there require code edits, palette colour shifts
+do not.
diff --git a/builder/assets/css/rouge.css b/builder/assets/css/rouge.css
deleted file mode 100644
index dfebb54b..00000000
--- a/builder/assets/css/rouge.css
+++ /dev/null
@@ -1,116 +0,0 @@
-.highlight table td { padding: 5px; }
-.highlight table pre { margin: 0; }
-.highlight, .highlight .w {
-  color: #24292f;
-  background-color: #f6f8fa;
-}
-.highlight .k, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt, .highlight .kv {
-  color: #cf222e;
-}
-.highlight .gr {
-  color: #f6f8fa;
-}
-.highlight .gd {
-  color: #82071e;
-  background-color: #ffebe9;
-}
-.highlight .nb {
-  color: #953800;
-}
-.highlight .nc {
-  color: #953800;
-}
-.highlight .no {
-  color: #953800;
-}
-.highlight .nn {
-  color: #953800;
-}
-.highlight .sr {
-  color: #116329;
-}
-.highlight .na {
-  color: #116329;
-}
-.highlight .nt {
-  color: #116329;
-}
-.highlight .gi {
-  color: #116329;
-  background-color: #dafbe1;
-}
-.highlight .ges {
-  font-weight: bold;
-  font-style: italic;
-}
-.highlight .kc {
-  color: #0550ae;
-}
-.highlight .l, .highlight .ld, .highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mx {
-  color: #0550ae;
-}
-.highlight .sb {
-  color: #0550ae;
-}
-.highlight .bp {
-  color: #0550ae;
-}
-.highlight .ne {
-  color: #0550ae;
-}
-.highlight .nl {
-  color: #0550ae;
-}
-.highlight .py {
-  color: #0550ae;
-}
-.highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm {
-  color: #0550ae;
-}
-.highlight .o, .highlight .ow {
-  color: #0550ae;
-}
-.highlight .gh {
-  color: #0550ae;
-  font-weight: bold;
-}
-.highlight .gu {
-  color: #0550ae;
-  font-weight: bold;
-}
-.highlight .s, .highlight .sa, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .sx, .highlight .s1, .highlight .ss {
-  color: #0a3069;
-}
-.highlight .nd {
-  color: #8250df;
-}
-.highlight .nf, .highlight .fm {
-  color: #8250df;
-}
-.highlight .err {
-  color: #f6f8fa;
-  background-color: #82071e;
-}
-.highlight .c, .highlight .ch, .highlight .cd, .highlight .cm, .highlight .cp, .highlight .cpf, .highlight .c1, .highlight .cs {
-  color: #6e7781;
-}
-.highlight .gl {
-  color: #6e7781;
-}
-.highlight .gt {
-  color: #6e7781;
-}
-.highlight .ni {
-  color: #24292f;
-}
-.highlight .si {
-  color: #24292f;
-}
-.highlight .ge {
-  color: #24292f;
-  font-style: italic;
-}
-.highlight .gs {
-  color: #24292f;
-  font-weight: bold;
-}
diff --git a/builder/book.mjs b/builder/book.mjs
index 5bd3e384..e01b0432 100644
--- a/builder/book.mjs
+++ b/builder/book.mjs
@@ -535,7 +535,7 @@ function renderBookHead(lang, siteTitle) {
 
   
   ${siteTitle}
-  
+  
   
 `;
 }
diff --git a/builder/highlight-theme.mjs b/builder/highlight-theme.mjs
new file mode 100644
index 00000000..39b6e72a
--- /dev/null
+++ b/builder/highlight-theme.mjs
@@ -0,0 +1,221 @@
+// twinBASIC syntax-highlight theme loader. Reads the vendored IDE
+// theme files under builder/themes/, derives a Symbol-keyed palette,
+// and emits both the renderer's scope -> class lookup and the matching
+// CSS stylesheet. Phase 3's highlight.mjs consumes classForScope to
+// translate Shiki's per-token scope chains to palette class names;
+// the same palette drives the build-time-generated tb-highlight.css
+// that styles those classes in light and dark mode.
+//
+// Replaces the legacy two-step indirection (`scripts/extract_theme_colors.py`
+// emitting SCSS partials under docs/_sass/custom/, consumed by Jekyll's
+// Sass pipeline). Under Phase 11 the .theme source feeds the renderer
+// directly; there is no Rouge-class intermediate naming step.
+
+import { promises as fs } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const DEFAULT_THEMES_DIR = path.join(__dirname, "themes");
+
+const PROPERTY_LINE = /^([A-Za-z][A-Za-z0-9_]*)\s*:\s*(.+?)\s*;?\s*$/;
+const SYMBOL_PROP =
+  /^Symbol([A-Za-z]+?)(Color|FontStyle|FontWeight|TextDecoration)$/;
+const COMMENT_RE = /\/\*[\s\S]*?\*\//g;
+
+const PROP_ORDER = ["Color", "FontStyle", "FontWeight", "TextDecoration"];
+const CSS_PROP = {
+  Color: "color",
+  FontStyle: "font-style",
+  FontWeight: "font-weight",
+  TextDecoration: "text-decoration",
+};
+
+// TextMate scope prefix -> twinBASIC theme Symbol. Same precedence as
+// the historic SCOPE_TO_ROUGE_CLASS table that lived in highlight.mjs:
+// more-specific scopes precede their parents. The renderer walks the
+// per-token scope chain inner-out and stops at the first prefix match.
+//
+// Scopes the IDE theme does not name (plain punctuation, generic
+// identifiers, illegal tokens, HTML tag names) are absent here; tokens
+// matching only those emit no  wrap and inherit the default text
+// colour from the surrounding .highlight rule.
+const SCOPE_TO_SYMBOL = [
+  ["punctuation.line-continuation",  "ContinuationCharacter"],
+  ["constant.language.boolean",      "LiteralBoolean"],
+  ["constant.language.empty",        "LiteralEmpty"],
+  ["constant.language.nothing",      "LiteralNothing"],
+  ["constant.language.null",         "LiteralNull"],
+  ["constant.numeric",               "LiteralNumeric"],
+  ["constant.other.date",            "LiteralDate"],
+  ["comment.block.preprocessor",     "ConditionalCompilationDirective"],
+  ["comment.line",                   "Comment"],
+  ["comment.block",                  "Comment"],
+  ["meta.preprocessor",              "ConditionalCompilationDirective"],
+  ["keyword.declaration",            "Keyword"],
+  ["keyword.operator.word",          "NamedOperator"],
+  ["keyword.operator",               "Operator"],
+  ["keyword.control",                "Keyword"],
+  ["keyword",                        "Keyword"],
+  ["storage.type.function.arrow",    "Operator"],
+  ["storage.type.function",          "Keyword"],
+  ["storage.modifier",               "Keyword"],
+  ["storage.type",                   "BuiltInDataType"],
+  ["entity.name.function",           "Function"],
+  ["entity.name.type",               "Class"],
+  ["entity.name.namespace",          "Module"],
+  ["entity.other.attribute-name",    "Attribute"],
+  ["variable",                       "Variable"],
+  ["support.function",               "Class"],
+  ["string.escape",                  "LiteralString"],
+  ["string.quoted.double",           "LiteralString"],
+];
+
+function parseTheme(text) {
+  const stripped = text.replace(COMMENT_RE, "");
+  const result = new Map();
+  for (const rawLine of stripped.split(/\r?\n/)) {
+    const m = PROPERTY_LINE.exec(rawLine.trim());
+    if (!m) continue;
+    const name = m[1];
+    const value = m[2].trim().replace(/;\s*$/, "").trim();
+    if (!value) continue; // empty `Name: ;` = inherit-from-parent, skip
+    result.set(name, value);
+  }
+  return result;
+}
+
+function symbolProps(theme) {
+  const grouped = new Map();
+  for (const [name, value] of theme) {
+    const m = SYMBOL_PROP.exec(name);
+    if (!m) continue;
+    const sym = m[1];
+    const prop = m[2];
+    let entry = grouped.get(sym);
+    if (!entry) {
+      entry = {};
+      grouped.set(sym, entry);
+    }
+    entry[prop] = value;
+  }
+  return grouped;
+}
+
+function propsKey(props) {
+  if (!props) return "_";
+  return PROP_ORDER.map((k) => `${k}=${props[k] || ""}`).join("|");
+}
+
+export async function loadHighlightTheme(themesDir = DEFAULT_THEMES_DIR) {
+  const [lightText, darkText] = await Promise.all([
+    fs.readFile(path.join(themesDir, "Light.theme"), "utf8"),
+    fs.readFile(path.join(themesDir, "Dark.theme"), "utf8"),
+  ]);
+  const light = symbolProps(parseTheme(lightText));
+  const dark = symbolProps(parseTheme(darkText));
+
+  // Deduped list of Symbols the renderer can land on.
+  const referenced = [];
+  const seen = new Set();
+  for (const [, sym] of SCOPE_TO_SYMBOL) {
+    if (seen.has(sym)) continue;
+    seen.add(sym);
+    referenced.push(sym);
+  }
+
+  // Group Symbols by their (Light props, Dark props) tuple so any two
+  // Symbols that share BOTH palettes' properties collapse to one
+  // class. The grouping key is a deterministic string suitable for
+  // sort -- the assigned classId stays stable across builds because
+  // the sort key is property-derived, not insertion-order-derived.
+  const symbolTuple = new Map();
+  for (const sym of referenced) {
+    symbolTuple.set(
+      sym,
+      propsKey(light.get(sym)) + "##" + propsKey(dark.get(sym)),
+    );
+  }
+  const uniqueTuples = [...new Set(symbolTuple.values())].sort();
+  const tupleToClass = new Map(
+    uniqueTuples.map((t, i) => [t, `c${i + 1}`]),
+  );
+
+  const symbolToClass = new Map();
+  for (const sym of referenced) {
+    symbolToClass.set(sym, tupleToClass.get(symbolTuple.get(sym)));
+  }
+
+  // Representative Symbol per class -- any one in the group works
+  // since group members share both palettes' properties.
+  const classToSample = new Map();
+  for (const sym of referenced) {
+    const cls = symbolToClass.get(sym);
+    if (!classToSample.has(cls)) classToSample.set(cls, sym);
+  }
+  // For human-readable rule comments: list every Symbol in each group.
+  const classToSymbols = new Map();
+  for (const sym of referenced) {
+    const cls = symbolToClass.get(sym);
+    if (!classToSymbols.has(cls)) classToSymbols.set(cls, []);
+    classToSymbols.get(cls).push(sym);
+  }
+
+  const scopeToClass = SCOPE_TO_SYMBOL.map(
+    ([scope, sym]) => [scope, symbolToClass.get(sym)],
+  );
+
+  function classForScope(scopes) {
+    for (let i = scopes.length - 1; i >= 0; i--) {
+      const scope = scopes[i];
+      for (const [prefix, cls] of scopeToClass) {
+        if (scope === prefix || scope.startsWith(prefix + ".")) return cls;
+      }
+    }
+    return null;
+  }
+
+  function classForSymbol(symbolName) {
+    return symbolToClass.get(symbolName) ?? null;
+  }
+
+  // CSS emit. One rule per (palette, classId). The dark palette nests
+  // under `html.dark-mode` so the chrome's theme toggle flips the
+  // syntax highlight in lockstep with the rest of the page.
+  const orderedClasses = uniqueTuples.map((t) => tupleToClass.get(t));
+  const symbolListComment = (cls) =>
+    classToSymbols.get(cls).map((s) => `Symbol${s}`).join(", ");
+
+  const renderRule = (selector, props, comment) => {
+    if (!props) return "";
+    const lines = [];
+    for (const k of PROP_ORDER) {
+      if (props[k]) lines.push(`  ${CSS_PROP[k]}: ${props[k]};`);
+    }
+    if (lines.length === 0) return "";
+    const c = comment ? `  /* ${comment} */` : "";
+    return `${selector} {${c}\n${lines.join("\n")}\n}\n`;
+  };
+
+  let css =
+    "/* twinBASIC syntax-highlight palette. Generated from\n" +
+    "   builder/themes/Light.theme + builder/themes/Dark.theme by\n" +
+    "   builder/highlight-theme.mjs. Do not hand-edit; regenerate by\n" +
+    "   running build.bat. */\n\n" +
+    "/* Light palette (root). */\n";
+  for (const cls of orderedClasses) {
+    const sym = classToSample.get(cls);
+    css += renderRule(`.highlight .${cls}`, light.get(sym), symbolListComment(cls));
+  }
+  css += "\n/* Dark palette (active under html.dark-mode). */\n";
+  for (const cls of orderedClasses) {
+    const sym = classToSample.get(cls);
+    css += renderRule(
+      `html.dark-mode .highlight .${cls}`,
+      dark.get(sym),
+      symbolListComment(cls),
+    );
+  }
+
+  return { classForScope, classForSymbol, css };
+}
diff --git a/builder/highlight.mjs b/builder/highlight.mjs
index cacc1340..8537f545 100644
--- a/builder/highlight.mjs
+++ b/builder/highlight.mjs
@@ -1,9 +1,12 @@
 // Phase 3 syntax highlighter. Wraps Shiki + the twinBASIC TextMate
-// grammar to produce Rouge-shaped HTML so the existing rouge.css keeps
-// working byte-for-byte. See builder/PLAN-3.md §5.10 + §7 for the full
-// spec.
+// grammar to produce themed 
 blocks; the per-token colour palette
+// comes from builder/highlight-theme.mjs (which reads the vendored
+// twinBASIC IDE .theme files). The renderer's class output and the
+// matching CSS share a single source of truth -- editing a .theme
+// file changes both halves of the pipeline.
+//
+// Wrapper shape (matches what the chrome's CSS selectors target):
 //
-// Wrapper shape (kept identical to Rouge's HTML formatter):
 //   
//
//
...spans...
@@ -14,72 +17,21 @@
 import { promises as fs } from "node:fs";
 import { createHighlighter } from "shiki";
 
-// TextMate scope prefix -> Rouge class. Entries are matched against the
-// per-token scope chain in order (most-specific scope first); the first
-// hit wins. Entries are language-agnostic (no trailing `.tb` / `.js` /
-// `.c`) so the same map handles every grammar Shiki loads. Ordering
-// matters: more-specific prefixes must precede their parents (e.g.
-// "comment.block.preprocessor" before "comment.block").
-const SCOPE_TO_ROUGE_CLASS = [
-  ["punctuation.line-continuation",          "lc"],
-  ["constant.language.boolean",              "lb"],
-  ["constant.language.empty",                "le"],
-  ["constant.language.nothing",              "ln"],
-  ["constant.language.null",                 "lu"],
-  ["constant.numeric.float",                 "mf"],
-  ["constant.numeric.integer",               "mi"],
-  ["constant.numeric",                       "m"],
-  ["constant.other.date",                    "ld"],
-  ["comment.line",                           "c1"],
-  ["comment.block.preprocessor",             "cp"],
-  ["comment.block",                          "cm"],
-  ["meta.preprocessor",                      "cp"],
-  ["keyword.declaration",                    "kd"],
-  ["keyword.operator.word",                  "ow"],
-  ["keyword.operator",                       "o"],
-  ["keyword.control",                        "k"],
-  ["keyword",                                "k"],
-  // JS arrow functions `=>` carry the more specific scope
-  // `storage.type.function.arrow.*`. Rouge treats them as Operator
-  // (`o`) -- match that BEFORE the broader `storage.type.function`
-  // rule below picks them up as kd.
-  ["storage.type.function.arrow",            "o"],
-  // Rouge's JS / Ruby / similar lexers classify `function`, `class`,
-  // `extends` etc. as Keyword::Declaration -- a separate token category
-  // from Keyword::Type. Match that for grammars that tag declarators as
-  // `storage.type.function.*`.
-  ["storage.type.function",                  "kd"],
-  // `async`/`await` / `static` / similar TextMate `storage.modifier`
-  // scopes are Keyword (not Declaration, not Type) in Rouge.
-  ["storage.modifier",                       "k"],
-  ["storage.type",                           "kt"],
-  ["entity.name.function",                   "nf"],
-  ["entity.name.type",                       "nc"],
-  ["entity.name.namespace",                  "nn"],
-  ["entity.other.attribute-name",            "na"],
-  ["entity.name.tag",                        "nt"],
-  ["variable",                               "nv"],
-  ["support.function",                       "nb"],
-  ["punctuation",                            "p"],
-  ["meta.brace",                             "p"],
-  ["string.escape",                          "se"],
-  ["string.quoted.double",                   "s"],
-  ["entity.name",                            "n"],
-  ["invalid.illegal",                        "err"],
-];
+import { loadHighlightTheme } from "./highlight-theme.mjs";
 
-// Languages we tokenise alongside the bundled tB grammar. Each name
-// must be a Shiki bundled-language identifier or a tB alias. `tb`'s
-// aliases collapse to "tb" for the wrapper class; every other entry
-// keeps its own name in the wrapper.
+// Fenced-info aliases that select the bundled tB grammar.
 const TB_ALIASES = new Set(["tb", "twinbasic", "vb", "vba"]);
-const SHIKI_BUNDLED_LANGS = ["js", "json", "ruby", "html", "yaml", "xml", "sql", "sh", "cpp", "c", "liquid"];
+const SHIKI_BUNDLED_LANGS = [
+  "js", "json", "ruby", "html", "yaml", "xml", "sql", "sh", "cpp", "c", "liquid",
+];
 
 let cached = null;
 
 export async function initHighlighter() {
   if (cached) return cached;
 
+  const theme = await loadHighlightTheme();
+
   let shiki = null;
   try {
     const grammarUrl = new URL("./twinbasic.tmLanguage.json", import.meta.url);
@@ -94,25 +46,21 @@ export async function initHighlighter() {
   }
 
   cached = {
-    render: (code, lang) => renderCodeBlock(shiki, code, lang),
+    render: (code, lang) => renderCodeBlock(shiki, theme, code, lang),
+    themeCss: theme.css,
   };
   return cached;
 }
 
-function renderCodeBlock(shiki, code, lang) {
+function renderCodeBlock(shiki, theme, code, lang) {
   const lower = (lang || "").toLowerCase();
   const isTb = TB_ALIASES.has(lower);
-  // Rouge wraps the code block with `language-` (the
-  // literal first token of the fence info). Two minor adjustments:
-  //   - The empty info string and `tb` aliases both resolve to the tB
-  //     lexer, but Rouge's CSS class is `language-`. Keep
-  //     `vb` / `vba` / `twinbasic` distinct in the wrapper. The
-  //     internal `tb` alias is only for the wrapper when no lang is
-  //     supplied (Jekyll's site default).
-  //   - `lang` may have whitespace from a ` ``` vb` style fence;
-  //     callers already trim, but stay defensive.
+  // The wrapper class is `language-`; keep `vb` / `vba` /
+  // `twinbasic` distinct in the wrapper even though they all route to
+  // the tB grammar internally. An empty info string lands as
+  // `language-plaintext`.
   const wrapperLang = lang ? lang.trim().toLowerCase() : "plaintext";
-  // Shiki language id for the actual tokenise call.
+
   let shikiLang = null;
   if (shiki) {
     if (isTb) {
@@ -122,9 +70,8 @@ function renderCodeBlock(shiki, code, lang) {
     }
   }
 
-  // Rouge always emits a trailing \n inside ; kramdown's GFM parser
-  // strips the user's trailing newline from the fence body and Rouge
-  // re-adds exactly one. Mirror that.
+  // The trailing \n inside  matches the rouge / kramdown shape:
+  // GFM strips the user's trailing newline; one is re-added here.
   const codeBody = code.endsWith("\n") ? code : code + "\n";
 
   let tokenizedHtml;
@@ -133,7 +80,7 @@ function renderCodeBlock(shiki, code, lang) {
       lang: shikiLang,
       includeExplanation: true,
     });
-    tokenizedHtml = renderRougeStyleSpans(lines, isTb);
+    tokenizedHtml = renderThemedSpans(lines, theme);
   } else {
     tokenizedHtml = escapeHtml(codeBody);
   }
@@ -142,58 +89,66 @@ function renderCodeBlock(shiki, code, lang) {
 }
 
 // Shiki's `codeToTokensBase` with `includeExplanation` returns
-// ThemedToken[][] where every top-level token has a per-line content
-// span. The token-level scope chain is coarse, but the `explanation`
-// array breaks it into per-segment entries with each segment's own
-// scope chain -- that's the granularity we need to emit Rouge-style
-// per-keyword/operator/string s.
-// Lift the scope-prefix lookup out of the hot loop -- one shallow
-// iteration over the chain replaces a doubly-nested loop in
-// `bestRougeClass`.
+// ThemedToken[][] where every top-level token also exposes a per-
+// segment scope chain inside its `explanation` array. The renderer
+// walks each segment, asks the theme for the matching palette class,
+// and emits coalesced run-spans:
+//
+//   (a) Adjacent same-class runs merge into one  so a multi-line
+//       block comment renders as a single coloured block.
+//   (b) Line-continuation runs absorb the leading whitespace of the
+//       next line, mirroring the tB lexer's `_[ \t]*\n[ \t]*` token
+//       shape -- one span covers both halves of the continuation.
+//   (c) Comment runs defer their trailing newline so a continuing
+//       comment on the next line merges into the same span; every
+//       other run flushes before the newline.
+function renderThemedSpans(lines, theme) {
+  const lcClass = theme.classForSymbol("ContinuationCharacter");
+  const cmClass = theme.classForSymbol("Comment");
 
-function renderRougeStyleSpans(lines, isTb) {
-  // Coalesce adjacent runs with the same Rouge class into one .
-  // Inter-line newlines are deferred -- if the next non-empty token
-  // shares the open run's class, the newline is absorbed into the span
-  // (the way Rouge keeps a multi-line block comment in one
-  // Comment::Multiline token); otherwise it flushes outside the span.
   const parts = [];
-  let runCls = undefined; // undefined = no run open; null = no-class run; string = class
+  let runCls = undefined;    // undefined = no run; null = unclassed run; string = class
   let runText = "";
   let pendingNewlines = "";
 
   const flush = () => {
-    if (runText === "") { runCls = undefined; return; }
-    parts.push(runCls ? `${runText}` : runText);
+    if (runText === "") {
+      runCls = undefined;
+      return;
+    }
+    parts.push(
+      runCls ? `${runText}` : runText,
+    );
     runText = "";
     runCls = undefined;
   };
   const append = (cls, text) => {
     if (text === "") return;
     if (runCls === undefined) {
-      // Pending newlines belong before the first content of a fresh run.
-      if (pendingNewlines !== "") { parts.push(pendingNewlines); pendingNewlines = ""; }
+      if (pendingNewlines !== "") {
+        parts.push(pendingNewlines);
+        pendingNewlines = "";
+      }
       runCls = cls;
       runText = text;
     } else if (cls === runCls) {
-      // Same class -- absorb any pending newline INTO the span and keep
-      // accumulating. This is what gives Rouge-shaped multi-line spans
-      // for block comments / multi-line strings.
-      if (pendingNewlines !== "") { runText += pendingNewlines; pendingNewlines = ""; }
+      // Same class -- absorb any pending newline INTO the span so
+      // multi-line same-class runs share a single coloured block.
+      if (pendingNewlines !== "") {
+        runText += pendingNewlines;
+        pendingNewlines = "";
+      }
       runText += text;
-    } else if (runCls === "lc" && cls === null && /^[ \t]+$/.test(text)) {
-      // Rouge's LineContinuation token is `_[ \t]*\n[ \t]*` -- i.e. it
-      // also absorbs the next line's leading whitespace into the same
-      //  element. Our TextMate `line-continuation`
-      // rule only matches `_[ \t]*$` (single-line); the next line's
-      // indent comes through as a separate cls=null whitespace token.
-      // Fold it into the open lc run instead of flushing.
+    } else if (runCls === lcClass && cls === null && /^[ \t]+$/.test(text)) {
+      // Fold the next line's leading whitespace into the open
+      // line-continuation span.
       runText += text;
     } else {
-      // Class change -- flush the open run, emit any pending newline
-      // OUTSIDE the span, then start the new run.
       flush();
-      if (pendingNewlines !== "") { parts.push(pendingNewlines); pendingNewlines = ""; }
+      if (pendingNewlines !== "") {
+        parts.push(pendingNewlines);
+        pendingNewlines = "";
+      }
       runCls = cls;
       runText = text;
     }
@@ -204,262 +159,42 @@ function renderRougeStyleSpans(lines, isTb) {
     for (const tok of line) {
       if (tok.explanation && tok.explanation.length > 0) {
         for (const ex of tok.explanation) {
-          // Walk scopes outer -> inner (least specific -> most specific
-          // per TextMate convention). The first scope whose prefix is
-          // mapped wins, which lets a parent comment.block scope override
-          // an inner punctuation.definition.comment marker the way Rouge
-          // emits a single Comment::Multiline token for `/* ... */`.
           const scopes = (ex.scopes || []).map((s) => s.scopeName);
-          let cls = bestRougeClass(scopes);
-          // Rouge's JS / Python / Ruby / C / etc. lexers tag identifiers
-          // as Name::Other (nx) rather than Name::Variable (nv); tB
-          // alone uses Name::Variable for `Dim X`-style declarations.
-          // Remap when the language isn't tB.
-          if (!isTb && cls === "nv") cls = "nx";
-          // Rouge's C / C++ lexers don't have a `variable.parameter`
-          // category; identifiers in parameter lists are just Name (n)
-          // even when Shiki tags them via `variable.parameter.*` in a
-          // lambda-capture context (e.g. `[out]` in IDL/COM API decls).
-          // Override for cpp / c scopes.
-          if (!isTb && (cls === "nx" || cls === "nv")
-              && /\.(cpp|c|h)$/.test(scopes[scopes.length - 1] || "")) {
-            cls = "n";
-          }
-          // Rouge's JS lexer has an explicit `builtins` list (Array,
-          // Boolean, Date, ..., window, document, navigator, ...)
-          // -- identifiers in that list are tagged Name::Builtin (nb).
-          // Shiki's JS grammar tags them as `variable.other.object.js`
-          // / `variable.other.readwrite.js` -> nv -> nx (via the
-          // remap above). Override to nb when the literal matches a
-          // known builtin and the scope is .js.
-          if (!isTb && (cls === "nx" || cls === null)
-              && /\.js$/.test(scopes[scopes.length - 1] || "")
-              && JS_BUILTINS.has(ex.content)) {
-            cls = "nb";
-          }
-          // Rouge's JS lexer classifies bare `var`/`let`/`const` as
-          // Keyword::Declaration (kd). In Shiki's JS grammar these
-          // tokens carry scope `storage.type.js` -- exactly the same
-          // shape (`storage.type.`, no specific keyword segment)
-          // tB uses for its type keywords (`storage.type.tb` ->
-          // String, Integer, ...). Disambiguate by language: for
-          // non-tB grammars whose specific scope is the bare
-          // `storage.type.` form, remap kt -> kd.
-          if (!isTb && cls === "kt" && STORAGE_TYPE_BARE_RE.test(scopes[scopes.length - 1] || "")) {
-            cls = "kd";
-          }
-          // Rouge's JS lexer has a quirk where a function-CALL
-          // identifier containing an uppercase letter is tagged
-          // Name::Class (nc) instead of Name::Function (nf):
-          // `Foo()` -> nc, `foo()` -> nf. Function DEFINITIONS keep
-          // `nf` regardless of case (the upstream regex at
-          // rouge/lexers/javascript.rb#185 only fires when the
-          // identifier is immediately followed by `(...)` AND is not in
-          // the `function (...)` declaration position handled by
-          // line 187). Distinguish by the parent scope chain --
-          // `meta.function-call.*` marks a call site,
-          // `meta.definition.function.*` marks a definition.
-          if (cls === "nf"
-              && /\.js$/.test(scopes[scopes.length - 1] || "")
-              && /^[$_]*\p{Lu}/u.test(ex.content)
-              && scopes.some((s) => s.startsWith("meta.function-call"))) {
-            cls = "nc";
-          }
-          // Rouge's numeric tokens are typed by content: Num::Integer
-          // (mi), Num::Float (mf), Num::Hex (mh), Num::Oct (mo), etc.
-          // Many Shiki grammars tag every numeric as the generic
-          // `constant.numeric.decimal.` (-> `m`) without
-          // distinguishing the subtype. Look at the literal text and
-          // upgrade `m` to the right Rouge bucket.
-          if (cls === "m") {
-            const trimmed = ex.content;
-            if (/^0[xX][0-9a-fA-F_]+$/.test(trimmed)) cls = "mh";
-            else if (/^0[oO][0-7_]+$/.test(trimmed)) cls = "mo";
-            else if (/^0[bB][01_]+$/.test(trimmed)) cls = "mb";
-            else if (/^[0-9_]+$/.test(trimmed)) cls = "mi";
-            else if (/^[0-9._eE+-]+$/.test(trimmed) && /\./.test(trimmed)) cls = "mf";
-          }
-          // Rouge's JS lexer (and several others) splits string tokens
-          // into delimiters (`"`, `'`) and content. The delimiter is
-          // tagged Str::Delimiter (`dl`), the content Str::Double (`s2`)
-          // or Str::Single (`s1`). tB and C alike use a single Str
-          // (`s`) for everything. Apply the split only when the scope
-          // chain has both a string scope and a definition marker --
-          // and only for grammars where Rouge does this split, i.e.
-          // JavaScript here. (C / Ruby / etc. stay with the parent
-          // string class via DEFINITION_MARKER_RE fallthrough.)
-          const lastScope = scopes[scopes.length - 1] || "";
-          if (!isTb && lastScope.startsWith("punctuation.definition.string") && /\.js$/.test(lastScope)) {
-            cls = "dl";
-          } else if (!isTb && /^string\.quoted\.double\.js$/.test(lastScope)) {
-            cls = "s2";
-          } else if (!isTb && /^string\.quoted\.single\.js$/.test(lastScope)) {
-            cls = "s1";
-          }
-          // Rouge's JS lexer parses `...`, `?`, `:` (and the other
-          // structural single-char tokens) as Punctuation, not Operator.
-          // TextMate / Shiki tags `...` as `keyword.operator.spread.*`,
-          // `?` ternary as `keyword.operator.ternary.*`, etc. -- all of
-          // which map to `o` via the generic `keyword.operator` rule.
-          // Remap the punctuation-shaped sub-scopes back to `p` when the
-          // grammar isn't tB.
-          if (!isTb && cls === "o" && NONTB_PUNCT_OPERATOR_RE.test(scopes[scopes.length - 1] || "")) {
-            cls = "p";
-          }
-          // Rouge tags unrecognised identifiers in the C / HTML / JS
-          // grammars as Name (class "n"); Shiki's bundled grammars
-          // leave them with no inner scope (just `source.`). When
-          // there's no class match and the token's trimmed text looks
-          // like an identifier, split off any leading / trailing
-          // whitespace and emit the identifier inside its own  span.
-          if (cls === null) {
-            const m = ex.content.match(/^(\s*)([A-Za-z_]\w*)(\s*)$/);
-            if (m) {
-              if (m[1]) append(null, escapeHtml(m[1]));
-              append("n", escapeHtml(m[2]));
-              if (m[3]) append(null, escapeHtml(m[3]));
-              continue;
-            }
-            // For C / C++ specifically, Shiki sometimes returns one big
-            // unclassified token covering a parameter list / declaration
-            // tail when the grammar's higher-level scope (e.g. a lambda
-            // capture) consumes the start but doesn't break out the
-            // rest. Rouge instead tokenises each character: identifiers
-            // -> Name (n), brackets / commas / dots -> Punctuation (p).
-            // Re-tokenise the bulk content with a lightweight scanner
-            // when the parent grammar is C / C++.
-            if (!isTb && CPP_LIKE_RE.test(scopes[scopes.length - 1] || "")
-                && SPLITTABLE_TOKEN_RE.test(ex.content)) {
-              for (const m2 of ex.content.matchAll(CPP_TOKEN_RE)) {
-                if (m2[1] !== undefined) append("n", escapeHtml(m2[1]));
-                else if (m2[2] !== undefined) append("p", escapeHtml(m2[2]));
-                else if (m2[3] !== undefined) append(null, m2[3]); // whitespace
-              }
-              continue;
-            }
-          }
+          const cls = theme.classForScope(scopes);
           append(cls, escapeHtml(ex.content));
         }
       } else {
         append(null, escapeHtml(tok.content));
       }
     }
-    // The Rouge twinBASIC lexer's line-continuation rule is
-    // `_[ \t]*\n[ \t]*` -- it ALSO absorbs the leading whitespace of
-    // the following line into the same . Append the
-    // newline to the open lc run here; the matching whitespace-absorb
-    // case in `append()` folds the next line's indent into the same
-    // run when its first token is a cls=null whitespace token.
-    //
-    // For block comments (cm), defer the newline so it can be merged
-    // into the next run if the same scope continues -- Rouge emits one
-    // Comment::Multiline span for a multi-line `/* ... */`. Every other
-    // class is single-line in Rouge's output even when the surface
-    // class matches across two adjacent lines (e.g. consecutive keyword
-    // lines), so flush the run before the newline.
-    if (runCls === "lc") {
-      append("lc", "\n");
-    } else if (runCls === "cm") {
+    // End of line:
+    //   - lc runs: fold the newline into the span; the next line's
+    //     leading whitespace is absorbed by the lcClass/cls=null
+    //     branch in append().
+    //   - comment runs: defer the newline so a continuing comment on
+    //     the next line can merge into the same span.
+    //   - everything else: flush and park the newline for the gap
+    //     between spans.
+    if (runCls === lcClass) {
+      append(lcClass, "\n");
+    } else if (runCls === cmClass) {
       pendingNewlines += "\n";
     } else {
       flush();
       pendingNewlines += "\n";
     }
   }
-  // End of input: flush any open run, then drop a single trailing
-  // newline (the Rouge trailing \n inside  is supplied by the
-  // codeBody adjustment in renderCodeBlock).
   flush();
   if (pendingNewlines !== "") {
+    // Drop the single trailing newline; renderCodeBlock already added
+    // one to codeBody.
     parts.push(pendingNewlines.slice(0, -1));
   }
   return parts.join("");
 }
 
-function bestRougeClass(scopes) {
-  // Walk inner -> outer (most-specific scope first). When a scope chain
-  // is `[source, string.quoted.double, string.escape]`, the inner
-  // `string.escape` should win over the parent `string.quoted.double`
-  // so Rouge's `""` shape lands.
-  //
-  // For `punctuation.definition.*` markers attached to begin/end of a
-  // container scope (comment, string, etc.), Rouge tokenises the whole
-  // container as ONE token; fall through to the parent so the marker
-  // gets the container's class. For markers OUTSIDE a container scope
-  // (e.g. `punctuation.definition.capture.begin.lambda.cpp` whose only
-  // parent is `source.cpp`), keep matching the marker as `punctuation`.
-  for (let i = scopes.length - 1; i >= 0; i--) {
-    const scope = scopes[i];
-    if (DEFINITION_MARKER_RE.test(scope) && hasContainerParent(scopes, i)) continue;
-    for (const [tmScope, cls] of SCOPE_TO_ROUGE_CLASS) {
-      if (scope === tmScope || scope.startsWith(tmScope + ".")) return cls;
-    }
-  }
-  return null;
-}
-
-const DEFINITION_MARKER_RE = /(^|\.)definition\./;
-
-// Rouge's non-tB lexers (JS / TS / Ruby / Python / C / etc.) classify
-// these tokens as Punctuation, not Operator. Shiki's TextMate grammars
-// tag them under `keyword.operator.*` which maps to `o` via the generic
-// rule -- remap to `p` for non-tB languages.
-const NONTB_PUNCT_OPERATOR_RE = /^keyword\.operator\.(spread|rest|ternary|optional)\b/;
-
-// The bare `storage.type.` scope (no specific keyword segment in
-// between). Used by JS for `let`/`const`/`var` and by tB for type
-// keywords. Disambiguated per-language in the renderer.
-const STORAGE_TYPE_BARE_RE = /^storage\.type\.[a-z]+$/;
-
-// Scopes whose unclassified bulk content should be re-tokenised by the
-// lightweight identifier-and-punctuation scanner below.
-const CPP_LIKE_RE = /\.(cpp|c|h|hpp|hxx)$/;
-
-// Cheap test: the bulk content actually contains punctuation worth
-// splitting (not just whitespace or a single identifier).
-const SPLITTABLE_TOKEN_RE = /[,;()\[\]{}.]|[A-Za-z_]\w+\s+[A-Za-z_]/;
-
-// One-pass scanner: identifier | punctuation | whitespace. Three
-// alternation groups so the regex engine reports which fired.
-const CPP_TOKEN_RE = /([A-Za-z_]\w*)|([,;()\[\]{}.])|(\s+)/g;
-
-// Rouge's JS lexer `builtins` list (rouge/lexers/javascript.rb#127).
-// Identifiers in this set are tagged Name::Builtin (`nb`).
-const JS_BUILTINS = new Set([
-  "Array", "Boolean", "Date", "Error", "Function", "Math", "netscape",
-  "Number", "Object", "Packages", "RegExp", "String", "sun", "decodeURI",
-  "decodeURIComponent", "encodeURI", "encodeURIComponent",
-  "eval", "isFinite", "isNaN", "parseFloat", "parseInt",
-  "document", "window", "navigator", "self", "global",
-  "Promise", "Set", "Map", "WeakSet", "WeakMap", "Symbol", "Proxy", "Reflect",
-  "Int8Array", "Uint8Array", "Uint8ClampedArray",
-  "Int16Array", "Uint16Array", "Uint16ClampedArray",
-  "Int32Array", "Uint32Array", "Uint32ClampedArray",
-  "Float32Array", "Float64Array", "DataView", "ArrayBuffer",
-]);
-
-// True when any outer scope in the chain is a `container` (one whose
-// top-level component matches a Rouge "single-token wrapper" element --
-// comment/string/heredoc). Used by bestRougeClass to decide whether a
-// `punctuation.definition.*` inner scope should fall through to the
-// container's class.
-const CONTAINER_TOP_LEVEL = new Set(["comment", "string"]);
-function hasContainerParent(scopes, fromIdx) {
-  for (let j = fromIdx - 1; j >= 0; j--) {
-    const top = scopes[j].split(".")[0];
-    if (CONTAINER_TOP_LEVEL.has(top)) return true;
-  }
-  return false;
-}
-
-// Matches a token whose content is entirely word characters (identifier-
-// like). Used by the non-tB fallback in renderRougeStyleSpans to tag
-// unrecognised identifiers as Rouge's Name token (class "n").
-const IDENT_FALLBACK_RE = /^[A-Za-z_][\w]*$/;
-
-// Rouge's HTML formatter escapes only `& < >` -- NOT quotes. Match that
-// so string literals inside code blocks keep their literal " character
-// and our bytes match Jekyll's verbatim.
+// Rouge's HTML formatter escapes only `& < >` -- not quotes. Match that
+// so string literals inside code blocks keep their literal " character.
 const HTML_ESCAPE = { "&": "&", "<": "<", ">": ">" };
 function escapeHtml(s) {
   return s.replace(/[&<>]/g, (c) => HTML_ESCAPE[c]);
diff --git a/builder/index.mjs b/builder/index.mjs
index e4252c7e..cac82ef0 100644
--- a/builder/index.mjs
+++ b/builder/index.mjs
@@ -143,7 +143,10 @@ async function main() {
   await templatePhase(pages, site);
   t.lap("template");
 
-  const writeStats = await writePhase(pages, staticFiles, { destRoot, dryRun });
+  const generatedAssets = highlighter.themeCss
+    ? [{ rel: "assets/css/tb-highlight.css", content: highlighter.themeCss }]
+    : [];
+  const writeStats = await writePhase(pages, staticFiles, { destRoot, dryRun, generatedAssets });
   t.lap("write");
 
   let auxStats = null;
diff --git a/builder/pdf.mjs b/builder/pdf.mjs
index 685f2b6c..5d5cdf5f 100644
--- a/builder/pdf.mjs
+++ b/builder/pdf.mjs
@@ -32,7 +32,7 @@ import {
 } from "./write.mjs";
 
 const PDF_SUFFIX = "-pdf";
-const REQUIRED_CSS = ["assets/css/print.css", "assets/css/rouge.css"];
+const REQUIRED_CSS = ["assets/css/print.css", "assets/css/tb-highlight.css"];
 const LIMIT = WRITE_LIMIT;
 
 // ---------------------------------------------------------------------------
diff --git a/builder/template.mjs b/builder/template.mjs
index a58e68fa..7b97da24 100644
--- a/builder/template.mjs
+++ b/builder/template.mjs
@@ -115,6 +115,7 @@ function renderHead(page, site, init) {
     `  \n` +
     `  \n` +
     `  \n` +
+    `  \n` +
     `  \n` +
     `  

A

B

C

D

\ No newline at end of file +

A

B

C

D

\ No newline at end of file From 3e55d6234327f616099a8a0817e6b99878bed822 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 27 May 2026 17:10:59 +0200 Subject: [PATCH 6/9] Phase 11 B5: server-side copy-code button builder/highlight.mjs now emits the `; + let cached = null; -export async function initHighlighter() { +export async function initHighlighter({ copyButton = true } = {}) { if (cached) return cached; const theme = await loadHighlightTheme(); @@ -45,14 +56,15 @@ export async function initHighlighter() { if (err.code !== "ENOENT") throw err; } + const copyButtonHtml = copyButton ? COPY_BUTTON_HTML : ""; cached = { - render: (code, lang) => renderCodeBlock(shiki, theme, code, lang), + render: (code, lang) => renderCodeBlock(shiki, theme, copyButtonHtml, code, lang), themeCss: theme.css, }; return cached; } -function renderCodeBlock(shiki, theme, code, lang) { +function renderCodeBlock(shiki, theme, copyButtonHtml, code, lang) { const lower = (lang || "").toLowerCase(); const isTb = TB_ALIASES.has(lower); // The wrapper class is `language-`; keep `vb` / `vba` / @@ -85,7 +97,7 @@ function renderCodeBlock(shiki, theme, code, lang) { tokenizedHtml = escapeHtml(codeBody); } - return `
${tokenizedHtml}
`; + return `
${copyButtonHtml}
${tokenizedHtml}
`; } // Shiki's `codeToTokensBase` with `includeExplanation` returns diff --git a/builder/index.mjs b/builder/index.mjs index fb4f25b8..1b359c1a 100644 --- a/builder/index.mjs +++ b/builder/index.mjs @@ -128,7 +128,9 @@ async function main() { // Build the shared markdown-it instance up front so Phase 2's SEO // pass and Phase 3's body renderer use the same configured renderer. // initHighlighter overlaps with the running git shell-outs above. - const highlighter = await initHighlighter(); + const highlighter = await initHighlighter({ + copyButton: config.enable_copy_code_button !== false, + }); const linkTables = buildLinkTables(pages); const baseurl = String(config.baseurl || ""); const staticFileSet = new Set(staticFiles.map((s) => s.srcRel)); From 68b0f1588e38a5b5e12bb1a78be2f4efdf3fa738 Mon Sep 17 00:00:00 2001 From: Kuba Sunderland-Ober Date: Wed, 27 May 2026 17:14:00 +0200 Subject: [PATCH 7/9] Phase 11 B10: search-data.js minification builder/offline.mjs's deriveOfflineSearchDataJs now parses the pretty-printed search-data JSON and re-stringifies it without indentation before wrapping as window.SEARCH_DATA. The online search-data.json keeps its pretty-printed shape (Phase 6 unchanged); only the offline tree's wrapper is minified. Real-world reduction on the current tree: 2.80 MB -> 2.70 MB (~100 KB / 3.6%). The PLAN-11 estimate of ~1.1 MB was optimistic; most of the file is content payload (preserved verbatim by JSON.parse/stringify), only the per-entry 4-space indentation collapses. ~75 KB from indent removal across 2587 entries plus ~25 KB from various brace/comma spacing. The lunr consumer (initSearch in just-the-docs.js) reads window.SEARCH_DATA via Object.entries; minification is invisible to it. Verified that the rewritten file parses cleanly and the 2587 entries are intact. --- WIP.md | 2 +- builder/FUTURE-WORK.md | 24 +++++++++++++----------- builder/PLAN-11.md | 2 +- builder/PLAN.md | 2 +- builder/offline.mjs | 10 ++++++++-- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/WIP.md b/WIP.md index e0c89dfc..94f2bfee 100644 --- a/WIP.md +++ b/WIP.md @@ -439,7 +439,7 @@ Phase 11 (in progress, see [builder/PLAN-11.md](builder/PLAN-11.md)) lands the o - **B2 — Shiki theme generated from `.theme` source.** Shipped. `scripts/extract_theme_colors.py` and `builder/assets/css/rouge.css` are gone; `builder/highlight-theme.mjs` parses the vendored `builder/themes/Light.theme` + `Dark.theme` files and emits `_site/assets/css/tb-highlight.css` at build time. Per-span class names switched from Rouge tokens (`k`, `s`, `mi`) to a palette scheme (`c1`, `c2`, …). `builder/highlight.mjs` shrank from ~470 lines to ~190 — the per-language Rouge-quirk overrides folded into the scope-to-Symbol table. - **B1 — Mermaid `.mmd` → `.svg` automation.** Shipped. `builder/mermaid.mjs` runs before Phase 1's discover, walks `docs/assets/images/mmd/*.mmd`, and invokes `mmdc` (via `npx --no-install` rooted at `builder/`) for any source whose `.svg` sibling is missing or older. The `.mmd` is now the canonical input; the SVG is a build artifact. Adds `@mermaid-js/mermaid-cli` as a devDependency in `builder/package.json`. The PDF render step already pulls in `puppeteer` at the repo root (and CI runs `npx puppeteer browsers install chrome --install-deps`), so `mermaid.mjs` reuses that cached Chrome via `PUPPETEER_EXECUTABLE_PATH` — no second Chrome download. A missing `mmdc` (e.g. someone never ran `npm install` in `builder/`) or a missing Chrome cache downgrades to a graceful warning; the existing on-disk SVG is retained and the build continues. - **B5 — Server-side copy-code button.** Shipped. `builder/highlight.mjs` emits the `