refacil-sdd-ai 5.2.2 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/NOTICE.md +46 -0
  2. package/README.md +209 -42
  3. package/agents/auditor.md +46 -0
  4. package/agents/debugger.md +41 -1
  5. package/agents/implementer.md +76 -10
  6. package/agents/investigator.md +36 -0
  7. package/agents/proposer.md +46 -2
  8. package/agents/tester.md +45 -8
  9. package/agents/validator.md +67 -13
  10. package/bin/cli.js +428 -83
  11. package/bin/postinstall.js +20 -0
  12. package/lib/bus/broker.js +121 -3
  13. package/lib/bus/spawn.js +189 -121
  14. package/lib/check-review.js +102 -0
  15. package/lib/codegraph-telemetry.js +135 -0
  16. package/lib/codegraph.js +273 -0
  17. package/lib/commands/autopilot.js +120 -0
  18. package/lib/commands/bus.js +29 -36
  19. package/lib/commands/compact.js +185 -46
  20. package/lib/commands/read-spec.js +352 -0
  21. package/lib/commands/sdd.js +429 -44
  22. package/lib/compact-guidance.js +122 -77
  23. package/lib/config.js +136 -0
  24. package/lib/global-paths.js +56 -20
  25. package/lib/hooks.js +32 -4
  26. package/lib/ide-detection.js +1 -1
  27. package/lib/ignore-files.js +5 -1
  28. package/lib/installer.js +202 -19
  29. package/lib/kapso.js +241 -0
  30. package/lib/methodology-migration-pending.js +13 -0
  31. package/lib/open-browser.js +32 -0
  32. package/lib/opencode-migrate.js +148 -0
  33. package/lib/opencode-plugin/index.js +84 -104
  34. package/lib/opencode-plugin/rules.js +236 -0
  35. package/lib/project-root.js +154 -0
  36. package/lib/repo-ide-sync.js +5 -0
  37. package/lib/spec-reader/lang.js +72 -0
  38. package/lib/spec-reader/md-parser.js +299 -0
  39. package/lib/spec-reader/session.js +139 -0
  40. package/lib/spec-reader/ui/app.js +685 -0
  41. package/lib/spec-reader/ui/index.html +59 -0
  42. package/lib/spec-reader/ui/mixed-lang.js +200 -0
  43. package/lib/spec-reader/ui/model-cache.js +117 -0
  44. package/lib/spec-reader/ui/style.css +294 -0
  45. package/lib/spec-reader/ui/supertonic-helper.js +565 -0
  46. package/lib/spec-sync.js +258 -0
  47. package/lib/test-scope.js +713 -0
  48. package/lib/testing-policy-sync.js +14 -2
  49. package/package.json +6 -3
  50. package/skills/apply/SKILL.md +39 -64
  51. package/skills/archive/SKILL.md +74 -48
  52. package/skills/ask/SKILL.md +43 -8
  53. package/skills/autopilot/SKILL.md +476 -0
  54. package/skills/bug/SKILL.md +52 -53
  55. package/skills/explore/SKILL.md +48 -1
  56. package/skills/guide/SKILL.md +31 -13
  57. package/skills/inbox/SKILL.md +9 -0
  58. package/skills/join/SKILL.md +1 -1
  59. package/skills/prereqs/BUS-CROSS-REPO.md +33 -16
  60. package/skills/prereqs/METHODOLOGY-CONTRACT.md +96 -17
  61. package/skills/prereqs/SKILL.md +1 -1
  62. package/skills/propose/SKILL.md +74 -19
  63. package/skills/read-spec/SKILL.md +76 -0
  64. package/skills/reply/SKILL.md +42 -9
  65. package/skills/review/SKILL.md +63 -25
  66. package/skills/review/checklist.md +2 -2
  67. package/skills/say/SKILL.md +40 -4
  68. package/skills/setup/SKILL.md +59 -5
  69. package/skills/setup/troubleshooting.md +11 -3
  70. package/skills/stats/SKILL.md +157 -0
  71. package/skills/test/SKILL.md +35 -10
  72. package/skills/up-code/SKILL.md +20 -13
  73. package/skills/update/SKILL.md +32 -1
  74. package/skills/verify/SKILL.md +78 -41
  75. package/templates/compact-guidance.md +10 -0
  76. package/templates/methodology-guide.md +5 -0
@@ -0,0 +1,59 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>refacil read-spec — TTS</title>
7
+ <link rel="stylesheet" href="/read-spec/style.css" />
8
+ <script type="importmap">
9
+ {
10
+ "imports": {
11
+ "onnxruntime-web": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0/dist/ort.min.mjs"
12
+ }
13
+ }
14
+ </script>
15
+ <script src="https://cdn.jsdelivr.net/npm/marked@15.0.4/marked.min.js" integrity="sha384-2NndlFAl0twi2nmu3w9mixodZFiN2fbxlakcYJF86bn1JeS1VB87/XqA7cqymzS+" crossorigin="anonymous"></script>
16
+ </head>
17
+ <body>
18
+ <header>
19
+ <h1>refacil read-spec</h1>
20
+ <p class="meta" id="source-meta">Loading session…</p>
21
+ </header>
22
+ <p id="status-bar">Preparing…</p>
23
+ <div class="layout">
24
+ <nav id="file-sidebar" class="file-sidebar hidden" aria-label="File navigation"></nav>
25
+ <main id="sections"></main>
26
+ <aside id="controls">
27
+ <div class="control-row">
28
+ <button type="button" id="prev-btn" disabled>Previous</button>
29
+ <button type="button" id="play-btn" disabled>Play</button>
30
+ <button type="button" id="next-btn" disabled>Next</button>
31
+ </div>
32
+ <label>
33
+ Language
34
+ <select id="lang-select"></select>
35
+ </label>
36
+ <label>
37
+ Voice
38
+ <select id="voice-select">
39
+ <option value="M1">M1</option>
40
+ <option value="M2">M2</option>
41
+ <option value="M3" selected>M3</option>
42
+ <option value="M4">M4</option>
43
+ <option value="M5">M5</option>
44
+ <option value="F1">F1</option>
45
+ <option value="F2">F2</option>
46
+ <option value="F3">F3</option>
47
+ <option value="F4">F4</option>
48
+ <option value="F5">F5</option>
49
+ </select>
50
+ </label>
51
+ <label>
52
+ Speed <span id="speed-val">1</span>
53
+ <input type="range" id="speed-range" min="0.9" max="1.5" step="0.05" value="1" />
54
+ </label>
55
+ </aside>
56
+ </div>
57
+ <script type="module" src="/read-spec/app.js"></script>
58
+ </body>
59
+ </html>
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Split Spanish spec text into TTS segments so English terms (paths, flags, code)
3
+ * are synthesized with lang=en and the rest with lang=es.
4
+ */
5
+
6
+ /** @typedef {{ lang: string, text: string }} LangSegment */
7
+
8
+ /** Pausa mínima recomendada entre trozos es/en al concatenar WAV (segundos). */
9
+ export const MIXED_LANG_GAP_SEC = 0.015;
10
+
11
+ /** Máximo de inferencias TTS por sección (cada una ~varios segundos en el navegador). */
12
+ export const MAX_TTS_SEGMENTS = 16;
13
+
14
+ /** Case-sensitive patterns only (no /i) to avoid marking Spanish words as English. */
15
+ const ENGLISH_SPAN_RE = new RegExp(
16
+ [
17
+ 'code block:\\s*\\w+',
18
+ '\\brefacil-sdd-ai(?:\\s+(?:read-spec|sync-spec|bus|archive|propose|verify|review|test|apply|help|config|--[\\w-]+|(?:[\\w@]+/)+[\\w./-]+|[\\w@][\\w./-]*\\.[a-z]{2,5}))*\\b',
19
+ '/refacil:[\\w-]+',
20
+ '--[\\w-]+',
21
+ '\\brefacil-[\\w-]+\\b',
22
+ '(?:\\b[\\w@]+/)+[\\w./-]+',
23
+ '\\b[\\w@][\\w./-]*\\.[a-z]{2,5}\\b',
24
+ // Uppercase acronyms: HTML, CSS, API, TTS, CDN, SRI, REST, DOM, JSON, CI, CD, PR, etc.
25
+ '\\b[A-Z]{2,}(?:[.-][A-Z0-9]+)*\\b',
26
+ '\\b(?:endpoint|payload|token|broker|queue|webhook|session|request|response|handler|middleware|callback|schema|query|buffer|stream|chunk|pipeline|runtime|deploy|build|merge|branch|commit|staging|timeout|mock|stub|parser|router|store|state|action|backend|frontend|gateway|socket|worker|event|hook|plugin|cache|blob|lambda|trigger|scaffold|view|model|repository|migration|seed|rollback|snapshot|diff|patch|header|cookie|cors|csrf|jwt|oauth|scoped|typed|mixin|decorator|hydrat)(?:e|s|es|ed|ing|er)?\\b',
27
+ // Common English tech words that appear in Spanish SDD specs
28
+ '\\b(?:scope|render|parse|fetch|input|output|format|sanitize|validate|serialize|deserialize|encode|decode|import|export|layout|sidebar|fallback|checkbox|dropdown|tooltip|toggle|badge|overlay|modal|toast|banner|loader|spinner|inline|bundle|chunk|wrapper|container|grid|flex|offset|margin|padding|breakpoint|viewport|hover|focus|click|submit|reset|drag|drop|resize|scroll|swipe|tab|panel|frame|slot|portal|fixture|snapshot|coverage|lint|typecheck|watch|serve|preview|release|tag|label|badge|draft|issue|ticket|pr|branch|rebase|squash|stash|worktree|submodule|monorepo|workspace|package|registry|artifact|manifest|lockfile|polyfill|shim|transpile|minify|tree.shak|tree-shak)(?:e|s|es|ed|ing|er)?\\b',
29
+ '\\b[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)+\\b',
30
+ '\\b[a-z]{2,}(?:-[a-z0-9]+)+\\b',
31
+ '\\b[a-z]+(?:[A-Z][a-z0-9]*)+\\b',
32
+ '\\b(?:IndexedDB|HuggingFace|Supertonic|onnxruntime(?:-web)?|Markdown|onnxruntime-web|Node\\.js|Human-in-the-Loop)\\b',
33
+ ].join('|'),
34
+ 'g',
35
+ );
36
+
37
+ /**
38
+ * @param {Float32Array|number[]} wav
39
+ * @param {number} sampleRate
40
+ * @param {number} [threshold]
41
+ * @returns {number[]}
42
+ */
43
+ export function trimTrailingSilence(wav, sampleRate, threshold = 0.006) {
44
+ const minTail = Math.floor(sampleRate * 0.02);
45
+ let end = wav.length;
46
+ while (end > minTail && Math.abs(wav[end - 1]) < threshold) {
47
+ end -= 1;
48
+ }
49
+ return wav.slice(0, end);
50
+ }
51
+
52
+ /**
53
+ * @param {Float32Array|number[]} wav
54
+ * @param {number} [threshold]
55
+ * @returns {number[]}
56
+ */
57
+ export function trimLeadingSilence(wav, threshold = 0.006) {
58
+ let start = 0;
59
+ while (start < wav.length && Math.abs(wav[start]) < threshold) {
60
+ start += 1;
61
+ }
62
+ return wav.slice(start);
63
+ }
64
+
65
+ /**
66
+ * @param {LangSegment[]} segments
67
+ * @returns {LangSegment[]}
68
+ */
69
+ function mergeAdjacentSegments(segments) {
70
+ /** @type {LangSegment[]} */
71
+ const out = [];
72
+ for (const seg of segments) {
73
+ const text = (seg.text || '').replace(/\s+/g, ' ').trim();
74
+ if (!text) continue;
75
+ const prev = out[out.length - 1];
76
+ if (prev && prev.lang === seg.lang) {
77
+ prev.text = `${prev.text} ${text}`;
78
+ } else {
79
+ out.push({ lang: seg.lang, text });
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ /** Minimum chars for a Spanish segment to stand alone; below this it is punctuation/conjunction. */
86
+ const MIN_SEGMENT_CHARS = 4;
87
+
88
+ /**
89
+ * Merge short Spanish segments (punctuation, conjunctions) into the previous segment
90
+ * to avoid generating a separate TTS inference — and a perceptible pause — for single commas
91
+ * or short connectors sandwiched between English spans.
92
+ * @param {LangSegment[]} segments
93
+ * @returns {LangSegment[]}
94
+ */
95
+ function mergeShortSegments(segments) {
96
+ const out = [];
97
+ for (const seg of segments) {
98
+ const text = (seg.text || '').replace(/\s+/g, ' ').trim();
99
+ if (!text) continue;
100
+ const prev = out[out.length - 1];
101
+ if (seg.lang === 'es' && text.length <= MIN_SEGMENT_CHARS && prev) {
102
+ prev.text = `${prev.text} ${text}`;
103
+ } else {
104
+ out.push({ lang: seg.lang, text });
105
+ }
106
+ }
107
+ return mergeAdjacentSegments(out);
108
+ }
109
+
110
+ /**
111
+ * Reduce fragmentación: fusiona pares adyacentes baratos priorizando pares del mismo idioma.
112
+ * @param {LangSegment[]} segments
113
+ * @param {number} [max]
114
+ * @returns {LangSegment[]}
115
+ */
116
+ export function consolidateSegments(segments, max = MAX_TTS_SEGMENTS) {
117
+ let list = segments.map((s) => ({ lang: s.lang, text: s.text }));
118
+ while (list.length > max) {
119
+ if (list.length < 2) break;
120
+
121
+ // Find the cheapest adjacent pair. Heavy penalty for cross-language merges.
122
+ let bestI = -1;
123
+ let bestScore = Infinity;
124
+ for (let i = 0; i < list.length - 1; i++) {
125
+ const combined = list[i].text.length + list[i + 1].text.length;
126
+ const crossPenalty = list[i].lang !== list[i + 1].lang ? 10000 : 0;
127
+ const score = combined + crossPenalty;
128
+ if (score < bestScore) {
129
+ bestScore = score;
130
+ bestI = i;
131
+ }
132
+ }
133
+
134
+ const a = list[bestI];
135
+ const b = list[bestI + 1];
136
+ // Same-lang: keep that lang. Cross-lang: keep Spanish (primary doc language).
137
+ const mergedLang = a.lang === b.lang ? a.lang : 'es';
138
+ list[bestI] = { lang: mergedLang, text: `${a.text} ${b.text}`.trim() };
139
+ list.splice(bestI + 1, 1);
140
+ list = mergeAdjacentSegments(list);
141
+ }
142
+ return list;
143
+ }
144
+
145
+ /**
146
+ * @param {number[][]} parts
147
+ * @returns {Float32Array}
148
+ */
149
+ export function concatWavParts(parts) {
150
+ const totalLen = parts.reduce((n, p) => n + p.length, 0);
151
+ const out = new Float32Array(totalLen);
152
+ let offset = 0;
153
+ for (const part of parts) {
154
+ out.set(part, offset);
155
+ offset += part.length;
156
+ }
157
+ return out;
158
+ }
159
+
160
+ /**
161
+ * @param {string} text
162
+ * @param {string} primaryLang Supertonic code (e.g. es, en)
163
+ * @returns {LangSegment[]}
164
+ */
165
+ export function splitBilingualText(text, primaryLang) {
166
+ const primary = (primaryLang || 'es').toLowerCase();
167
+ if (!text || primary !== 'es') {
168
+ return [{ lang: primary, text: (text || '').trim() }];
169
+ }
170
+
171
+ /** @type {LangSegment[]} */
172
+ const segments = [];
173
+ let lastIndex = 0;
174
+ const re = new RegExp(ENGLISH_SPAN_RE.source, ENGLISH_SPAN_RE.flags);
175
+ let match;
176
+ while ((match = re.exec(text)) !== null) {
177
+ if (match[0].length === 0) {
178
+ re.lastIndex += 1;
179
+ continue;
180
+ }
181
+ const before = text.slice(lastIndex, match.index);
182
+ if (before.trim()) {
183
+ segments.push({ lang: 'es', text: before });
184
+ }
185
+ segments.push({ lang: 'en', text: match[0] });
186
+ lastIndex = re.lastIndex;
187
+ }
188
+ const tail = text.slice(lastIndex);
189
+ if (tail.trim()) {
190
+ segments.push({ lang: 'es', text: tail });
191
+ }
192
+
193
+ const merged = mergeAdjacentSegments(segments);
194
+ if (!merged.length) {
195
+ return [{ lang: 'es', text: text.trim() }];
196
+ }
197
+ // Absorb short Spanish fragments (punctuation, conjunctions) before counting segments.
198
+ const compacted = mergeShortSegments(merged);
199
+ return consolidateSegments(compacted);
200
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * IndexedDB cache for HuggingFace Supertonic model assets (first download only).
3
+ */
4
+
5
+ const DB_NAME = 'refacil-read-spec-tts-v1';
6
+ const STORE = 'assets';
7
+
8
+ function openDb() {
9
+ return new Promise((resolve, reject) => {
10
+ const req = indexedDB.open(DB_NAME, 1);
11
+ req.onerror = () => reject(req.error);
12
+ req.onsuccess = () => resolve(req.result);
13
+ req.onupgradeneeded = () => {
14
+ const db = req.result;
15
+ if (!db.objectStoreNames.contains(STORE)) {
16
+ db.createObjectStore(STORE);
17
+ }
18
+ };
19
+ });
20
+ }
21
+
22
+ /**
23
+ * @param {string} url
24
+ * @returns {Promise<ArrayBuffer | null>}
25
+ */
26
+ async function getCached(url) {
27
+ try {
28
+ const db = await openDb();
29
+ return new Promise((resolve, reject) => {
30
+ const tx = db.transaction(STORE, 'readonly');
31
+ const req = tx.objectStore(STORE).get(url);
32
+ req.onsuccess = () => resolve(req.result || null);
33
+ req.onerror = () => reject(req.error);
34
+ });
35
+ } catch (_) {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * @param {string} url
42
+ * @param {ArrayBuffer} data
43
+ */
44
+ async function putCached(url, data) {
45
+ try {
46
+ const db = await openDb();
47
+ return new Promise((resolve, reject) => {
48
+ const tx = db.transaction(STORE, 'readwrite');
49
+ tx.objectStore(STORE).put(data, url);
50
+ tx.oncomplete = () => resolve();
51
+ tx.onerror = () => reject(tx.error);
52
+ });
53
+ } catch (_) {
54
+ // ignore cache write failures
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Wrap global fetch to cache Supertone/supertonic-3 assets in IndexedDB.
60
+ * @param {(loaded: number, total: number, label: string) => void} [onProgress]
61
+ */
62
+ export function installCachedFetch(onProgress) {
63
+ const nativeFetch = window.fetch.bind(window);
64
+ const hfPrefix = 'huggingface.co/Supertone/supertonic-3';
65
+
66
+ window.fetch = async (input, init) => {
67
+ const url = typeof input === 'string' ? input : input.url;
68
+ if (!url.includes(hfPrefix)) {
69
+ return nativeFetch(input, init);
70
+ }
71
+
72
+ if (!navigator.onLine) {
73
+ const cached = await getCached(url);
74
+ if (cached) {
75
+ return new Response(cached, { status: 200, headers: { 'content-type': guessContentType(url) } });
76
+ }
77
+ throw new Error('Internet required for first model download. Connect and reload.');
78
+ }
79
+
80
+ const cached = await getCached(url);
81
+ if (cached) {
82
+ if (onProgress) onProgress(1, 1, url.split('/').pop());
83
+ return new Response(cached, { status: 200, headers: { 'content-type': guessContentType(url) } });
84
+ }
85
+
86
+ const res = await nativeFetch(input, init);
87
+ if (!res.ok) return res;
88
+ const buf = await res.arrayBuffer();
89
+ await putCached(url, buf);
90
+ if (onProgress) onProgress(1, 1, url.split('/').pop());
91
+ return new Response(buf, { status: 200, headers: { 'content-type': guessContentType(url) } });
92
+ };
93
+ }
94
+
95
+ /**
96
+ * @param {string} url
97
+ * @returns {string}
98
+ */
99
+ function guessContentType(url) {
100
+ if (url.endsWith('.json')) return 'application/json';
101
+ if (url.endsWith('.onnx')) return 'application/octet-stream';
102
+ return 'application/octet-stream';
103
+ }
104
+
105
+ export async function hasAnyCachedModel() {
106
+ try {
107
+ const db = await openDb();
108
+ return new Promise((resolve) => {
109
+ const tx = db.transaction(STORE, 'readonly');
110
+ const req = tx.objectStore(STORE).count();
111
+ req.onsuccess = () => resolve(req.result > 0);
112
+ req.onerror = () => resolve(false);
113
+ });
114
+ } catch (_) {
115
+ return false;
116
+ }
117
+ }
@@ -0,0 +1,294 @@
1
+ :root {
2
+ --bg: #0f1419;
3
+ --panel: #1a2332;
4
+ --text: #e7ecf3;
5
+ --muted: #8b9cb3;
6
+ --accent: #3d8bfd;
7
+ --active: #1e3a5f;
8
+ --border: #2a3548;
9
+ }
10
+
11
+ * {
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ margin: 0;
17
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
18
+ background: var(--bg);
19
+ color: var(--text);
20
+ line-height: 1.5;
21
+ }
22
+
23
+ header {
24
+ padding: 1rem 1.25rem;
25
+ border-bottom: 1px solid var(--border);
26
+ background: var(--panel);
27
+ }
28
+
29
+ header h1 {
30
+ margin: 0 0 0.25rem;
31
+ font-size: 1.25rem;
32
+ }
33
+
34
+ header .meta {
35
+ color: var(--muted);
36
+ font-size: 0.875rem;
37
+ }
38
+
39
+ #status-bar {
40
+ padding: 0.75rem 1.25rem;
41
+ background: #15202b;
42
+ border-bottom: 1px solid var(--border);
43
+ font-size: 0.875rem;
44
+ }
45
+
46
+ #status-bar.error {
47
+ color: #ff8a8a;
48
+ }
49
+
50
+ #backend-badge {
51
+ display: inline-block;
52
+ margin-left: 0.5rem;
53
+ padding: 0.1rem 0.4rem;
54
+ border-radius: 4px;
55
+ background: #2d4a22;
56
+ font-size: 0.75rem;
57
+ }
58
+
59
+ .layout {
60
+ display: grid;
61
+ grid-template-columns: auto 1fr 320px;
62
+ gap: 0;
63
+ min-height: calc(100vh - 120px);
64
+ }
65
+
66
+ @media (max-width: 900px) {
67
+ .layout {
68
+ grid-template-columns: 1fr;
69
+ }
70
+ .file-sidebar { grid-column: auto; }
71
+ #sections { grid-column: auto; }
72
+ #controls { grid-column: auto; }
73
+ }
74
+
75
+ .file-sidebar {
76
+ grid-column: 1;
77
+ }
78
+
79
+ #sections {
80
+ grid-column: 2;
81
+ padding: 1rem 1.25rem 2rem;
82
+ overflow-y: auto;
83
+ }
84
+
85
+ #controls {
86
+ grid-column: 3;
87
+ }
88
+
89
+ .section {
90
+ margin-bottom: 1.25rem;
91
+ padding: 0.75rem 1rem;
92
+ border-radius: 8px;
93
+ border: 1px solid transparent;
94
+ }
95
+
96
+ .section.active {
97
+ background: var(--active);
98
+ border-color: var(--accent);
99
+ }
100
+
101
+ .section h2 {
102
+ margin: 0 0 0.5rem;
103
+ font-size: 1rem;
104
+ color: var(--accent);
105
+ }
106
+
107
+ /* Plain-text fallback: only apply pre-wrap when section-body holds raw text (no HTML tags) */
108
+ .section .section-body:not(:has(> p, > ul, > ol, > h1, > h2, > h3, > h4, > blockquote, > pre, > table)) {
109
+ white-space: pre-wrap;
110
+ }
111
+
112
+ /* Markdown-rendered HTML elements produced by marked */
113
+ .section h1,
114
+ .section h2,
115
+ .section h3,
116
+ .section h4 {
117
+ margin: 0.75rem 0 0.25rem;
118
+ font-size: 0.95rem;
119
+ color: var(--accent);
120
+ }
121
+
122
+ .section ul,
123
+ .section ol {
124
+ margin: 0.4rem 0 0.4rem 1.25rem;
125
+ padding: 0;
126
+ }
127
+
128
+ .section li {
129
+ margin-bottom: 0.2rem;
130
+ }
131
+
132
+ .section code {
133
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
134
+ font-size: 0.85em;
135
+ background: rgba(255, 255, 255, 0.08);
136
+ border-radius: 3px;
137
+ padding: 0.1em 0.3em;
138
+ }
139
+
140
+ .section pre {
141
+ background: rgba(255, 255, 255, 0.05);
142
+ border: 1px solid var(--border);
143
+ border-radius: 6px;
144
+ padding: 0.6rem 0.8rem;
145
+ overflow-x: auto;
146
+ font-size: 0.82rem;
147
+ margin: 0.5rem 0;
148
+ }
149
+
150
+ .section pre code {
151
+ background: transparent;
152
+ padding: 0;
153
+ }
154
+
155
+ .section blockquote {
156
+ border-left: 3px solid var(--accent);
157
+ margin: 0.5rem 0;
158
+ padding: 0.25rem 0.75rem;
159
+ color: var(--muted);
160
+ }
161
+
162
+ .section table {
163
+ border-collapse: collapse;
164
+ width: 100%;
165
+ font-size: 0.85rem;
166
+ margin: 0.5rem 0;
167
+ }
168
+
169
+ .section thead {
170
+ background: rgba(255, 255, 255, 0.06);
171
+ }
172
+
173
+ .section th,
174
+ .section td {
175
+ border: 1px solid var(--border);
176
+ padding: 0.3rem 0.5rem;
177
+ text-align: left;
178
+ }
179
+
180
+ .section tr:nth-child(even) td {
181
+ background: rgba(255, 255, 255, 0.03);
182
+ }
183
+
184
+ .section strong {
185
+ font-weight: 600;
186
+ color: var(--text);
187
+ }
188
+
189
+ .section em {
190
+ font-style: italic;
191
+ color: var(--muted);
192
+ }
193
+
194
+ .section p {
195
+ margin: 0.3rem 0;
196
+ }
197
+
198
+ #controls {
199
+ padding: 1rem;
200
+ background: var(--panel);
201
+ border-left: 1px solid var(--border);
202
+ }
203
+
204
+ #controls label {
205
+ display: block;
206
+ margin-bottom: 0.75rem;
207
+ font-size: 0.8rem;
208
+ color: var(--muted);
209
+ }
210
+
211
+ #controls select,
212
+ #controls input[type="range"] {
213
+ width: 100%;
214
+ margin-top: 0.25rem;
215
+ }
216
+
217
+ #lang-select:disabled {
218
+ opacity: 1;
219
+ cursor: default;
220
+ pointer-events: none;
221
+ background: #e8f0fe;
222
+ color: #1a5fc8;
223
+ border-color: #c5d3f0;
224
+ font-weight: 600;
225
+ }
226
+
227
+ .control-row {
228
+ display: flex;
229
+ gap: 0.5rem;
230
+ margin-bottom: 1rem;
231
+ }
232
+
233
+ .control-row button {
234
+ flex: 1;
235
+ padding: 0.5rem;
236
+ border: 1px solid var(--border);
237
+ border-radius: 6px;
238
+ background: #243044;
239
+ color: var(--text);
240
+ cursor: pointer;
241
+ }
242
+
243
+ .control-row button:hover {
244
+ background: #2f3f56;
245
+ }
246
+
247
+ .control-row button:disabled {
248
+ opacity: 0.5;
249
+ cursor: not-allowed;
250
+ }
251
+
252
+ #play-btn {
253
+ background: var(--accent);
254
+ border-color: var(--accent);
255
+ color: #fff;
256
+ font-weight: 600;
257
+ }
258
+
259
+ .hidden {
260
+ display: none;
261
+ }
262
+
263
+ .file-sidebar {
264
+ padding: 0.75rem;
265
+ background: var(--panel);
266
+ border-right: 1px solid var(--border);
267
+ overflow-y: auto;
268
+ }
269
+
270
+ .sidebar-file-btn {
271
+ display: block;
272
+ width: 100%;
273
+ padding: 0.4rem 0.6rem;
274
+ margin-bottom: 0.25rem;
275
+ border: 1px solid transparent;
276
+ border-radius: 5px;
277
+ background: transparent;
278
+ color: var(--muted);
279
+ font-size: 0.82rem;
280
+ text-align: left;
281
+ cursor: pointer;
282
+ }
283
+
284
+ .sidebar-file-btn:hover {
285
+ background: #243044;
286
+ color: var(--text);
287
+ }
288
+
289
+ .sidebar-file-btn.active {
290
+ background: var(--active);
291
+ border-color: var(--accent);
292
+ color: var(--text);
293
+ font-weight: 600;
294
+ }