skills-viewer 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -20,6 +20,8 @@ npx skills-viewer
20
20
  - **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, updated date, or token cost
21
21
  - **Diagnostics** — an _unused_ badge (no recorded use within the transcript retention window) with an all / used / unused filter, plus static description lint: missing / too-short / too-long descriptions, missing trigger conditions ("use when …") that make auto-invocation unlikely, and name-echo descriptions
22
22
  - **Token cost** — since every name + description is injected into each session, the estimated token overhead is shown per item, per scope, and as a per-session total for the current project
23
+ - **AI trigger diagnosis** — one click asks haiku whether the description is likely to trigger auto-invocation, lists concrete issues, and proposes an improved description you can apply with one click (cached by content hash)
24
+ - **Edit in the browser** — inline editor for SKILL.md / commands / agents (project & user scopes) with mtime conflict detection and a one-generation backup in `~/.cache/skills-viewer/backups/`
23
25
  - **What's changed** — a banner shows items added / updated / removed since your last launch (baseline advances only when you dismiss it); the CLI prints a one-line summary at startup too
24
26
  - **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
25
27
  - **Usage stats** — invocation counts and last-used dates aggregated from Claude Code session transcripts (`~/.claude/projects/`), covering both user-typed `/skill` calls and model-invoked Skill tool calls
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ /*
3
+ * AI 発動診断: description が「モデルの自動発動判断」に足るかを haiku で診断し、
4
+ * 指摘と改善版 description を返す。summary.ts と同じく content hash + lang で
5
+ * ~/.cache/skills-viewer/diagnoses.json にキャッシュし、内容が変わらない限り再生成しない。
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.loadDiagnoses = loadDiagnoses;
42
+ exports.parseDiagnosis = parseDiagnosis;
43
+ exports.diagnoseOne = diagnoseOne;
44
+ exports.attachDiagnoses = attachDiagnoses;
45
+ const fs = __importStar(require("node:fs"));
46
+ const os = __importStar(require("node:os"));
47
+ const path = __importStar(require("node:path"));
48
+ const summary_1 = require("./summary");
49
+ const DIAG_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'diagnoses.json');
50
+ function loadDiagnoses() {
51
+ try {
52
+ return JSON.parse(fs.readFileSync(DIAG_FILE, 'utf8'));
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ function saveDiagnoses(store) {
59
+ fs.mkdirSync(path.dirname(DIAG_FILE), { recursive: true });
60
+ fs.writeFileSync(DIAG_FILE, JSON.stringify(store, null, 1));
61
+ }
62
+ function buildPrompt(name, content, lang) {
63
+ if (lang === 'ja') {
64
+ return ('以下は Claude Code の skill 定義ファイルです。モデルは frontmatter の name と description **だけ**を見て、' +
65
+ 'このスキルを自動発動するか判断します(本文は発動後にしか読まれません)。\n' +
66
+ 'description の「発動性」を診断し、次の JSON だけを出力してください(前置き・コードフェンス不要):\n' +
67
+ '{"verdict": "good" | "weak",\n' +
68
+ ' "issues": ["問題点(日本語で各25字程度、最大4件。無ければ空配列)"],\n' +
69
+ ' "improved": "改善版 description(日本語)。本文から読み取れる発動条件(〜するとき、〜と頼まれたとき等)を必ず含め、200字以内"}\n\n' +
70
+ 'verdict は、description だけでモデルが適切な場面で発動を判断できるなら good、曖昧・条件不足なら weak。\n' +
71
+ 'improved は verdict が good でも、より良くできるなら改善版を出すこと(現状のままで良ければ元の文をそのまま)。\n\n' +
72
+ '# skill: ' +
73
+ name +
74
+ '\n\n' +
75
+ content);
76
+ }
77
+ return ('Below is a Claude Code skill definition file. The model decides whether to auto-invoke this skill ' +
78
+ 'based **only** on the frontmatter name and description (the body is read only after invocation).\n' +
79
+ 'Diagnose how well the description triggers invocation, and output ONLY this JSON (no preamble, no code fences):\n' +
80
+ '{"verdict": "good" | "weak",\n' +
81
+ ' "issues": ["problem (about 10 words each, max 4; empty array if none)"],\n' +
82
+ ' "improved": "improved description in English; MUST include trigger conditions readable from the body (\\"use when …\\"), max 400 chars"}\n\n' +
83
+ 'verdict: good if the description alone lets the model invoke at the right moments; weak if vague or missing conditions.\n' +
84
+ 'improved: even when verdict is good, propose a better version if possible (return the original text if it is already ideal).\n\n' +
85
+ '# skill: ' +
86
+ name +
87
+ '\n\n' +
88
+ content);
89
+ }
90
+ /* haiku の出力を検証つきでパース(壊れた出力は reject して UI にエラー表示) */
91
+ function parseDiagnosis(text) {
92
+ const stripped = text
93
+ .replace(/^```(?:json)?\s*/i, '')
94
+ .replace(/```\s*$/, '')
95
+ .trim();
96
+ const j = JSON.parse(stripped);
97
+ const verdict = j.verdict === 'good' ? 'good' : 'weak';
98
+ const issues = Array.isArray(j.issues)
99
+ ? j.issues.filter((x) => typeof x === 'string').map((s) => s.slice(0, 80))
100
+ : [];
101
+ const improved = String(j.improved || '').trim();
102
+ if (!improved)
103
+ throw new Error('empty improved description');
104
+ return { verdict, issues: issues.slice(0, 4), improved };
105
+ }
106
+ async function diagnoseOne(realPath, name, lang) {
107
+ const hash = (0, summary_1.contentHash)(realPath);
108
+ const store = loadDiagnoses();
109
+ const cached = store[realPath];
110
+ if (cached && cached.hash === hash && cached.lang === lang) {
111
+ return { verdict: cached.verdict, issues: cached.issues, improved: cached.improved };
112
+ }
113
+ const content = fs.readFileSync(realPath, 'utf8').slice(0, 12000);
114
+ const result = parseDiagnosis(await (0, summary_1.runHaiku)(buildPrompt(name, content, lang)));
115
+ store[realPath] = { ...result, hash, lang, generatedAt: new Date().toISOString() };
116
+ saveDiagnoses(store);
117
+ return result;
118
+ }
119
+ /* スキャン結果にキャッシュ済み診断を付与(内容が変わっていれば付けない) */
120
+ function attachDiagnoses(sections, lang) {
121
+ const store = loadDiagnoses();
122
+ for (const s of sections) {
123
+ for (const it of s.items) {
124
+ const cached = store[it.path];
125
+ if (cached &&
126
+ cached.lang === lang &&
127
+ it.path &&
128
+ fs.existsSync(it.path) &&
129
+ cached.hash === (0, summary_1.contentHash)(it.path)) {
130
+ it.aiDiagnosis = {
131
+ verdict: cached.verdict,
132
+ issues: cached.issues,
133
+ improved: cached.improved,
134
+ };
135
+ }
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /*
3
+ * ブラウザ内編集の実体: SKILL.md / command / agent の保存と description 差し替え。
4
+ * 本ツール初の「ファイル内容の書き換え」なので、
5
+ * - パス検証は manage.ts の assertManagedPath(.claude/skills|commands|agents 限定、plugin 拒否)に乗せる
6
+ * - mtime による競合検出(エディタ等での外部変更を上書きしない)
7
+ * - 保存前に ~/.cache/skills-viewer/backups/ へ1世代バックアップ(trash と同じ思想)
8
+ * を必ず通す。
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.assertEditableMd = assertEditableMd;
45
+ exports.doSave = doSave;
46
+ exports.replaceDescription = replaceDescription;
47
+ exports.doApplyDescription = doApplyDescription;
48
+ const fs = __importStar(require("node:fs"));
49
+ const os = __importStar(require("node:os"));
50
+ const path = __importStar(require("node:path"));
51
+ const manage_1 = require("./manage");
52
+ const errors_1 = require("./errors");
53
+ /* テストから隔離できるよう env で差し替え可能にする(通常運用では未設定)。
54
+ モジュール読み込み後に env を設定するテストのため、参照は呼び出し時に行う */
55
+ const backupDir = () => process.env.SKILLS_VIEWER_BACKUP_DIR ||
56
+ path.join(os.homedir(), '.cache', 'skills-viewer', 'backups');
57
+ const MAX_CONTENT = 512 * 1024; // request body 上限(1MB)より小さく、md として十分
58
+ function assertEditableMd(p) {
59
+ const { real } = (0, manage_1.assertManagedPath)(p);
60
+ if (!real.endsWith('.md'))
61
+ throw new errors_1.ApiError('not-md', real);
62
+ return real;
63
+ }
64
+ /* 上書き前の内容を1世代だけ退避(ファイルごとに1枠、保存のたびに更新) */
65
+ function backupOnce(real, content) {
66
+ try {
67
+ const dir = backupDir();
68
+ fs.mkdirSync(dir, { recursive: true });
69
+ fs.writeFileSync(path.join(dir, real.replace(/[^a-zA-Z0-9.]/g, '-')), content);
70
+ }
71
+ catch {
72
+ /* バックアップ失敗で保存自体は止めない */
73
+ }
74
+ }
75
+ /* mtime 競合チェック → バックアップ → 書き込み、の共通経路 */
76
+ function writeGuarded(real, next, baseMtime) {
77
+ const st = fs.statSync(real);
78
+ if (typeof baseMtime === 'number' && st.mtimeMs !== baseMtime) {
79
+ throw new errors_1.ApiError('edit-conflict', real);
80
+ }
81
+ backupOnce(real, fs.readFileSync(real, 'utf8'));
82
+ fs.writeFileSync(real, next);
83
+ return { mtime: fs.statSync(real).mtimeMs };
84
+ }
85
+ function doSave(data) {
86
+ const real = assertEditableMd(data.src);
87
+ const content = data.content;
88
+ if (typeof content !== 'string' || !content.trim())
89
+ throw new errors_1.ApiError('empty-content');
90
+ if (content.length > MAX_CONTENT)
91
+ throw new errors_1.ApiError('content-too-large');
92
+ const baseMtime = typeof data.baseMtime === 'number' ? data.baseMtime : undefined;
93
+ return { ok: true, ...writeGuarded(real, content, baseMtime) };
94
+ }
95
+ /*
96
+ * frontmatter の description だけを差し替える(AI 改善案のワンクリック適用用)。
97
+ * - 既存の description 行(block scalar 含む)を置き換え
98
+ * - description キーが無ければ frontmatter 先頭(name の次)に挿入
99
+ * - frontmatter 自体が無ければ新設
100
+ * 値は常に YAML double-quote(JSON.stringify)で書き、改行は空白に潰す。
101
+ */
102
+ function replaceDescription(raw, desc) {
103
+ const value = 'description: ' + JSON.stringify(desc.replace(/\s*\n\s*/g, ' ').trim());
104
+ const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n?)/);
105
+ if (!fm)
106
+ return `---\n${value}\n---\n\n` + raw;
107
+ const lines = fm[1].split(/\r?\n/);
108
+ const out = [];
109
+ let replaced = false;
110
+ let i = 0;
111
+ while (i < lines.length) {
112
+ const m = lines[i].match(/^description:\s*(.*)$/);
113
+ if (!m || replaced) {
114
+ out.push(lines[i]);
115
+ i++;
116
+ continue;
117
+ }
118
+ out.push(value);
119
+ replaced = true;
120
+ i++;
121
+ if (['|', '>', '|-', '>-'].includes(m[1].trim())) {
122
+ // block scalar の継続行(インデント行・空行)を捨てる
123
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].trim() === ''))
124
+ i++;
125
+ }
126
+ }
127
+ if (!replaced) {
128
+ // name の直後、無ければ先頭に挿入
129
+ const nameIdx = out.findIndex((l) => /^name:/.test(l));
130
+ out.splice(nameIdx >= 0 ? nameIdx + 1 : 0, 0, value);
131
+ }
132
+ return '---\n' + out.join('\n') + '\n---' + fm[2] + raw.slice(fm[0].length);
133
+ }
134
+ function doApplyDescription(data) {
135
+ const real = assertEditableMd(data.src);
136
+ if (typeof data.description !== 'string' || !data.description.trim()) {
137
+ throw new errors_1.ApiError('empty-content');
138
+ }
139
+ const prev = fs.readFileSync(real, 'utf8');
140
+ const baseMtime = typeof data.baseMtime === 'number' ? data.baseMtime : undefined;
141
+ return {
142
+ ok: true,
143
+ ...writeGuarded(real, replaceDescription(prev, data.description), baseMtime),
144
+ };
145
+ }
@@ -48,6 +48,8 @@ const scan_1 = require("./scan");
48
48
  const usage_1 = require("./usage");
49
49
  const summary_1 = require("./summary");
50
50
  const manage_1 = require("./manage");
51
+ const edit_1 = require("./edit");
52
+ const diagnose_1 = require("./diagnose");
51
53
  const snapshot_1 = require("./snapshot");
52
54
  const errors_1 = require("./errors");
53
55
  const locale_1 = require("./locale");
@@ -154,6 +156,7 @@ function collect(cwd, lang) {
154
156
  }
155
157
  }
156
158
  }
159
+ (0, diagnose_1.attachDiagnoses)(sections, lang);
157
160
  const aiStale = (0, summary_1.staleItems)(sections, lang).length;
158
161
  const targets = [
159
162
  { label: 'user skills', sub: '~/.claude/skills/', path: scan_1.HOME },
@@ -211,7 +214,11 @@ function handleApi(req, res, cwd) {
211
214
  return send(200, (0, summary_1.summaryStatus)());
212
215
  if (url.pathname === '/api/file') {
213
216
  const real = (0, manage_1.assertReadableMd)(url.searchParams.get('src') || '');
214
- return send(200, { content: fs.readFileSync(real, 'utf8') });
217
+ // mtime は編集画面の競合検出(/api/save baseMtime)に使う
218
+ return send(200, {
219
+ content: fs.readFileSync(real, 'utf8'),
220
+ mtime: fs.statSync(real).mtimeMs,
221
+ });
215
222
  }
216
223
  throw new errors_1.ApiError('unknown-endpoint', url.pathname);
217
224
  }
@@ -238,6 +245,18 @@ function handleApi(req, res, cwd) {
238
245
  }
239
246
  const lang = langOf(data.lang);
240
247
  try {
248
+ if (url.pathname === '/api/save')
249
+ return send(200, (0, edit_1.doSave)(data));
250
+ if (url.pathname === '/api/apply-description')
251
+ return send(200, (0, edit_1.doApplyDescription)(data));
252
+ if (url.pathname === '/api/diagnose') {
253
+ const real = (0, manage_1.assertReadableMd)(data.src);
254
+ const name = data.name || path.basename(path.dirname(real));
255
+ (0, diagnose_1.diagnoseOne)(real, name, lang)
256
+ .then((d) => send(200, { ok: true, ...d }))
257
+ .catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
258
+ return;
259
+ }
241
260
  if (url.pathname === '/api/changes-ack') {
242
261
  (0, snapshot_1.ackChanges)((0, scan_1.scanSections)(cwd, lang));
243
262
  return send(200, { ok: true });
@@ -70,7 +70,18 @@ function parseFrontmatter(raw) {
70
70
  value = block.join('\n').trim();
71
71
  }
72
72
  else {
73
- value = value.replace(/^["']|["']$/g, '');
73
+ if (/^".*"$/.test(value)) {
74
+ // double-quote は JSON 互換エスケープ(\" 等)を解釈する(edit.ts の書き込みと対)
75
+ try {
76
+ value = JSON.parse(value);
77
+ }
78
+ catch {
79
+ value = value.slice(1, -1);
80
+ }
81
+ }
82
+ else {
83
+ value = value.replace(/^'|'$/g, '');
84
+ }
74
85
  i++;
75
86
  }
76
87
  meta[key] = value;
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
41
  exports.loadSummaries = loadSummaries;
42
42
  exports.contentHash = contentHash;
43
+ exports.runHaiku = runHaiku;
43
44
  exports.summarizeOne = summarizeOne;
44
45
  exports.parseAnalysis = parseAnalysis;
45
46
  exports.saveSummary = saveSummary;
@@ -145,22 +146,12 @@ function buildPrompt(it, refs, content, lang) {
145
146
  content);
146
147
  }
147
148
  /*
148
- * 1 skill haiku で分析し {summary, invocation, invocationReason, relations} を返す。
149
- * relations の候補(refs)は静的解析で抽出済みの既知 skill 名のみに制限し、幻覚を防ぐ。
149
+ * claude CLI headless (haiku) にプロンプトを渡して生テキストを得る共通実行部。
150
+ * --tools '' で全ツールを無効化: SKILL.md は clone したリポジトリ由来もあり得るため、
151
+ * 本文に指示が仕込まれていても純粋なテキスト生成の外に出られないようにする
150
152
  */
151
- function summarizeOne(it, lang) {
153
+ function runHaiku(prompt) {
152
154
  return new Promise((resolve, reject) => {
153
- let content;
154
- try {
155
- content = fs.readFileSync(it.path, 'utf8').slice(0, 12000);
156
- }
157
- catch (e) {
158
- return reject(e);
159
- }
160
- const refs = it.refs || [];
161
- const prompt = buildPrompt(it, refs, content, lang);
162
- // --tools '' で全ツールを無効化: SKILL.md は clone したリポジトリ由来もあり得るため、
163
- // 本文に指示が仕込まれていても純粋なテキスト生成の外に出られないようにする
164
155
  const child = (0, node_child_process_1.spawn)('claude', ['-p', '--model', 'haiku', '--tools', ''], {
165
156
  stdio: ['pipe', 'pipe', 'pipe'],
166
157
  });
@@ -184,11 +175,21 @@ function summarizeOne(it, lang) {
184
175
  const text = out.trim();
185
176
  if (code !== 0 || !text)
186
177
  return reject(new Error(errOut.trim().slice(0, 200) || 'claude exited ' + code));
187
- resolve(parseAnalysis(text, refs));
178
+ resolve(text);
188
179
  });
189
180
  child.stdin.end(prompt);
190
181
  });
191
182
  }
183
+ /*
184
+ * 1 skill を haiku で分析し {summary, invocation, invocationReason, relations} を返す。
185
+ * relations の候補(refs)は静的解析で抽出済みの既知 skill 名のみに制限し、幻覚を防ぐ。
186
+ */
187
+ async function summarizeOne(it, lang) {
188
+ const content = fs.readFileSync(it.path, 'utf8').slice(0, 12000);
189
+ const refs = it.refs || [];
190
+ const text = await runHaiku(buildPrompt(it, refs, content, lang));
191
+ return parseAnalysis(text, refs);
192
+ }
192
193
  /* haiku の出力を検証つきでパース。壊れていたら全文を summary として扱う */
193
194
  function parseAnalysis(text, refs) {
194
195
  const stripped = text
@@ -1 +1 @@
1
- *{box-sizing:border-box}body{margin:0;background:#faf9f7;color:#1b1a17;font-family:Helvetica Neue,system-ui,-apple-system,Hiragino Sans,sans-serif}button{font-family:inherit}.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}.wrap{margin:0 auto;min-height:100vh}.wrap.fixed{max-width:1200px}.wrap.fixed .grid{grid-template-columns:repeat(3,minmax(0,1fr))}@media(max-width:900px){.wrap.fixed .grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.hd{padding:28px 32px 0}.hd .t-row{display:flex;align-items:baseline;gap:12px}.hd h1{font-size:22px;font-weight:700;letter-spacing:-.01em;margin:0}.hd h1 a{color:#1b1a17;text-decoration:none}.hd h1 a:hover{color:#57534a}.hd .sub{font-size:12px;color:#8a8579}.hd .count{margin-left:auto;font-size:12px;color:#8a8579}.hd input.q{margin-top:16px;width:100%;padding:11px 14px;border:1px solid #ddd8ce;border-radius:10px;font-size:13.5px;background:#fff;color:#1b1a17;outline:none;font-family:inherit}.controls{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;align-items:center}.chip{padding:6px 14px;border-radius:999px;font-size:12px;cursor:pointer;background:#fff;border:1px solid #ddd8ce;color:#6b6558}.chip.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.grp-label{display:flex;align-items:center;gap:7px;font-size:12px;color:#57534a;cursor:pointer;margin-left:auto}.grp-label input{accent-color:#1b1a17;width:15px;height:15px;cursor:pointer}.ctrl-sep{width:1px;height:18px;background:#ddd8ce;margin:0 4px;align-self:center}.sel{-moz-appearance:none;appearance:none;-webkit-appearance:none;font-family:inherit;font-size:12px;color:#6b6558;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:6px 30px 6px 14px;cursor:pointer;outline:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%238a8579'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}.sel:focus{border-color:#1b1a17}.seg{display:inline-flex;border:1px solid #ddd8ce;border-radius:999px;overflow:hidden;background:#fff}.seg button{border:0;background:transparent;font-family:inherit;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.seg button+button{border-left:1px solid #efede8}.seg button.on{background:#1b1a17;color:#fff}.set-label{font-size:12px;font-weight:600;color:#57534a;margin-bottom:10px}.set-options{display:flex;flex-direction:column;gap:8px;margin-bottom:20px}.set-option{display:flex;align-items:center;gap:9px;font-size:13px;cursor:pointer}.set-option input{accent-color:#1b1a17}.set-scheme{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#a39d90;margin-left:auto}.set-input{font-family:ui-monospace,Menlo,monospace;font-size:12px;padding:8px 10px;border:1px solid #ddd8ce;border-radius:8px;outline:none;margin-left:24px}.set-input:focus{border-color:#1b1a17}.pbtn.primary{background:#1b1a17;border-color:#1b1a17;color:#fff}.pbtn.primary:hover{background:#3a372f;border-color:#3a372f}.sec-h{display:flex;align-items:center;gap:8px;margin:14px 0 6px;position:sticky;top:0;z-index:5;background:#faf9f7;padding:10px 0 8px}.sec-h .sq{width:8px;height:8px;border-radius:2px;flex:none}.sec-h .lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#57534a}.sec-h .n{font-size:11px;color:#a39d90}.sec-h .ln{flex:1;height:1px;background:#e8e4dc}.sec-h.sm{margin:8px 0 4px;padding:8px 0 6px}.sec-h.sm .lbl{font-size:10.5px}.grid-pad{padding:6px 32px 32px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:18px 20px;display:flex;flex-direction:column;gap:10px;cursor:pointer;text-align:left;min-width:0}.card:hover{border-color:#b9b2a3;box-shadow:0 2px 8px #1b1a170f}.card .top{display:flex;align-items:baseline;gap:8px}.card .nm{font-size:15px;font-weight:600;color:#1b1a17;word-break:break-all}.card .ver{margin-left:auto;font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;white-space:nowrap}.card .desc{font-size:13px;line-height:1.65;color:#57534a;margin:0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.card .usage{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;background:#f4f1ea;border-radius:6px;padding:8px 10px;color:#6b6558;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .meta{display:flex;gap:8px;font-size:10.5px;color:#8a8579}.card .meta span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card .meta .meta-r{margin-left:auto;flex:none;display:inline-flex;gap:8px;align-items:center}.scope-mini{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:600;letter-spacing:.04em;color:#8a8579;margin-bottom:-4px}.dot5{width:6px;height:6px;border-radius:2px;flex:none}.empty{text-align:center;padding:44px;font-size:13px;color:#8a8579}.ai-mark{color:#b0836a;font-size:11px}.kbadge{font-size:9.5px;font-weight:700;padding:2px 8px;border-radius:999px;background:#f1efe9;color:#8a8579;white-space:nowrap;flex:none;vertical-align:3px;margin-left:8px}.same-actions{margin-left:auto;display:inline-flex;gap:6px}.pbtn.sm{font-size:11px;padding:3px 10px}.pbtn.sm.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.diff-wrap{margin-top:10px}.diff-legend{display:flex;gap:14px;align-items:center;font-size:11px;margin-bottom:6px}.d-del-mark{color:#b0392e}.d-add-mark{color:#1f8a5b}.d-count{color:#8a8579;margin-left:auto}.diff{border:1px solid #e8e4dc;border-radius:8px;overflow-x:auto;background:#fff;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.7;max-height:480px;overflow-y:auto}.d-line{padding:0 12px;white-space:pre}.d-add{background:#1f8a5b14;color:#16603f}.d-del{background:#b0392e12;color:#8c2d24}.d-skip{padding:2px 12px;color:#a39d90;background:#faf9f7;text-align:center;border-top:1px solid #efede8;border-bottom:1px solid #efede8}.inv-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none}.inv-human{color:#1f8a5b;background:#1f8a5b1a}.inv-agent{color:#7b5bd6;background:#7b5bd61a}.inv-both{color:#b8862f;background:#c07a1f21}.inv-ai{opacity:.85}.rel-chips{display:flex;flex-wrap:wrap;gap:8px}.rel-chip{display:inline-flex;align-items:center;gap:7px;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:7px 12px;cursor:pointer}.rel-chip:hover{background:#f4f1ea}.rel-chip .rt{font-size:9.5px;font-weight:700;text-transform:uppercase;color:#8a8579}.rel-chip .rn{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#1b1a17}.rel-chip.missing{border-style:dashed;cursor:default}.rel-chip.missing .rn{color:#a39d90}.rel-chip.missing .rm{font-size:9.5px;color:#b0836a}.md-wrap{display:flex;align-items:stretch}.left-col{width:340px;flex:none;border-right:1px solid #eeeae2;padding:8px 20px 28px 32px;position:sticky;top:0;max-height:100vh;overflow-y:auto;overscroll-behavior:contain}.ccard{display:flex;flex-direction:column;gap:4px;width:100%;background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:12px 14px;cursor:pointer;text-align:left;margin-bottom:8px}.ccard:hover{border-color:#b9b2a3}.ccard.sel{border-color:#1b1a17;box-shadow:0 1px 5px #1b1a171a}.ccard .r1{display:flex;align-items:center;gap:7px}.ccard .dot7{width:7px;height:7px;border-radius:50%;flex:none}.ccard .nm{font-size:13.5px;font-weight:600;color:#1b1a17;word-break:break-all}.ccard .ver{margin-left:auto;font-size:10px;color:#a39d90;font-family:ui-monospace,Menlo,monospace}.ccard .d1{font-size:11.5px;color:#8a8579;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pane{flex:1;min-width:0;max-width:980px;padding:20px 36px 36px 32px}.back{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:7px 14px;cursor:pointer}.back:hover{background:#f4f1ea}.meta-row{display:flex;align-items:center;gap:10px;margin-top:20px;position:relative;flex-wrap:wrap}.badge{font-size:10.5px;font-weight:700;padding:4px 11px;border-radius:999px}.m-ver{font-size:11px;color:#8a8579;font-family:ui-monospace,Menlo,monospace}.m-upd{margin-left:auto;font-size:11px;color:#8a8579}.pbtn{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.pbtn:hover{background:#f4f1ea}.pbtn.danger{color:#b0392e;border-color:#e4ccc7}.pbtn.danger:hover{background:#fbf1ef}.d-name{font-size:26px;font-weight:700;color:#1b1a17;margin:14px 0 0;word-break:break-all}.tabs{display:flex;gap:20px;margin-top:18px;border-bottom:1px solid #eeeae2}.tab{background:none;border:0;font-size:13px;color:#8a8579;padding:8px 2px 10px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.tab.on{color:#1b1a17;font-weight:600;border-bottom-color:#1b1a17}.sec-t{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;margin:26px 0 10px}.full-desc{font-size:13.5px;line-height:1.8;color:#57534a;margin:0;white-space:pre-wrap}.ex-block{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#f4f1ea;border-radius:8px;padding:10px 13px;color:#57534a;margin-bottom:8px;overflow-x:auto;white-space:pre}.f-row{display:flex;align-items:center;gap:9px;border-bottom:1px solid #eeeae2;padding:8px 4px}.f-row .sq6{width:6px;height:6px;border-radius:2px;background:#c5bfb2;flex:none}.f-row .p{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#57534a;word-break:break-all}.fm-box{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#8a8579;background:#faf9f7;border:1px solid #e8e4dc;border-radius:8px;padding:12px 14px;white-space:pre-wrap;word-break:break-word;margin:18px 0}.md-body{font-size:12.5px;line-height:1.9;color:#3a372f}.md-body h1,.md-body h2{border-bottom:1px solid #eeeae2;padding-bottom:6px}.md-body h1{font-size:19px;margin:26px 0 10px;color:#1b1a17}.md-body h2{font-size:16px;margin:24px 0 10px;color:#1b1a17}.md-body h3{font-size:14px;margin:20px 0 8px;color:#1b1a17}.md-body h4{font-size:13px;margin:16px 0 6px;color:#1b1a17}.md-body p{margin:8px 0}.md-body ul,.md-body ol{margin:8px 0;padding-left:22px}.md-body li{margin:3px 0}.md-body pre{background:#f4f1ea;border-radius:8px;padding:12px 14px;overflow-x:auto;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.6}.md-body code{font-family:ui-monospace,Menlo,monospace;font-size:.9em;background:#f4f1ea;color:#8a5a2b;border-radius:4px;padding:1px 5px}.md-body pre code{background:none;color:inherit;padding:0}.md-body blockquote{border-left:3px solid #ddd8ce;margin:8px 0;padding:2px 0 2px 14px;color:#6b6558}.md-body hr{border:0;border-top:1px solid #eeeae2;margin:18px 0}.md-body a{color:#2a6fdb}.drop{position:absolute;top:calc(100% + 6px);right:0;width:260px;background:#fff;border:1px solid #ddd8ce;border-radius:10px;box-shadow:0 10px 28px #1b1a1724;z-index:30;padding:8px 0;max-height:320px;overflow-y:auto}.drop .dh{font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;padding:6px 14px 4px}.drop .di{display:block;width:100%;text-align:left;background:none;border:0;padding:8px 14px;cursor:pointer}.drop .di:hover{background:#f4f1ea}.drop .di .l1{font-size:12.5px;font-weight:600;color:#1b1a17}.drop .di .l2{font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;word-break:break-all}.overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#1b1a1759;z-index:40;display:flex;align-items:center;justify-content:center}.modal{width:390px;max-width:90vw;background:#fff;border-radius:14px;padding:26px 28px}.modal h3{font-size:15px;font-weight:700;margin:0 0 10px;color:#1b1a17}.modal p{font-size:12.5px;line-height:1.7;color:#57534a;margin:0 0 20px}.modal .btns{display:flex;justify-content:flex-end;gap:8px}.modal .del{background:#b0392e;border:1px solid #b0392e;color:#fff;border-radius:999px;font-size:12px;padding:7px 16px;cursor:pointer}.modal .del:hover{background:#962f27}@media(max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.left-col{display:none}}@media(min-width:1480px){.grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1960px){.grid{grid-template-columns:repeat(5,minmax(0,1fr))}}.unused-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none;color:#5b7285;background:#5b72851f}.warn-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none;color:#b0432f;background:#c848301a;position:relative;cursor:help}.card .top .top-r{margin-left:auto;display:inline-flex;align-items:center;gap:8px;flex:none}.card .top .top-r .ver{margin-left:0}.card .chips{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:-4px}.card .chips .kbadge{margin-left:0;vertical-align:0}.warn-badge .tip{display:none;position:absolute;top:calc(100% + 6px);right:-6px;z-index:40;width:250px;padding:10px 12px;border-radius:8px;background:#1b1a17;color:#f4f1ea;font-size:11.5px;font-weight:400;line-height:1.6;white-space:normal;text-align:left;box-shadow:0 4px 14px #1b1a1740}.warn-badge:hover .tip{display:block}.warn-badge .tip .tip-t{display:block;font-weight:700;margin-bottom:4px}.warn-badge .tip .tip-line{display:block;margin-top:3px;padding-left:12px;text-indent:-12px}.warn-badge .tip .tip-line:before{content:"· "}.card .meta .tok,.tok-total{font-family:ui-monospace,Menlo,monospace;white-space:nowrap}.hd .tok-total{cursor:help}.sec-h .sec-tok{font-size:11px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;cursor:help}.lint-row{display:flex;gap:8px;align-items:baseline;font-size:13px;line-height:1.65;color:#57534a;padding:2px 0}.lint-row .lint-mark{color:#b0432f;flex:none}.chg-banner{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 0 -4px;padding:10px 14px;border:1px solid #e3ddd0;border-radius:10px;background:#faf7ef;font-size:12px;color:#57534a}.chg-title{font-weight:700;flex:none}.chg-group{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.chg-label{font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap}.chg-label.add{color:#1f8a5b;background:#1f8a5b1a}.chg-label.upd{color:#2a6fdb;background:#2a6fdb1a}.chg-label.del{color:#b0432f;background:#c848301a}.chg-chip{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;padding:2px 8px;border-radius:6px;border:1px solid #ddd8ce;background:#fff;color:#57534a;cursor:pointer}.chg-chip:hover{border-color:#b9b2a3}.chg-chip.gone{cursor:default;text-decoration:line-through;color:#a39d90}.chg-more{font-size:11px;color:#8a8579}.chg-ack{margin-left:auto;flex:none}.spark-wrap{display:flex;align-items:flex-end;gap:10px;margin:2px 0 6px}.spark rect{fill:#e3ddd0}.spark rect.on{fill:#1f8a5b}.spark-label{font-size:10.5px;color:#8a8579;white-space:nowrap}
1
+ *{box-sizing:border-box}body{margin:0;background:#faf9f7;color:#1b1a17;font-family:Helvetica Neue,system-ui,-apple-system,Hiragino Sans,sans-serif}button{font-family:inherit}.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}.wrap{margin:0 auto;min-height:100vh}.wrap.fixed{max-width:1200px}.wrap.fixed .grid{grid-template-columns:repeat(3,minmax(0,1fr))}@media(max-width:900px){.wrap.fixed .grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.hd{padding:28px 32px 0}.hd .t-row{display:flex;align-items:baseline;gap:12px}.hd h1{font-size:22px;font-weight:700;letter-spacing:-.01em;margin:0}.hd h1 a{color:#1b1a17;text-decoration:none}.hd h1 a:hover{color:#57534a}.hd .sub{font-size:12px;color:#8a8579}.hd .count{margin-left:auto;font-size:12px;color:#8a8579}.hd input.q{margin-top:16px;width:100%;padding:11px 14px;border:1px solid #ddd8ce;border-radius:10px;font-size:13.5px;background:#fff;color:#1b1a17;outline:none;font-family:inherit}.controls{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;align-items:center}.chip{padding:6px 14px;border-radius:999px;font-size:12px;cursor:pointer;background:#fff;border:1px solid #ddd8ce;color:#6b6558}.chip.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.grp-label{display:flex;align-items:center;gap:7px;font-size:12px;color:#57534a;cursor:pointer;margin-left:auto}.grp-label input{accent-color:#1b1a17;width:15px;height:15px;cursor:pointer}.ctrl-sep{width:1px;height:18px;background:#ddd8ce;margin:0 4px;align-self:center}.sel{-moz-appearance:none;appearance:none;-webkit-appearance:none;font-family:inherit;font-size:12px;color:#6b6558;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:6px 30px 6px 14px;cursor:pointer;outline:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%238a8579'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}.sel:focus{border-color:#1b1a17}.seg{display:inline-flex;border:1px solid #ddd8ce;border-radius:999px;overflow:hidden;background:#fff}.seg button{border:0;background:transparent;font-family:inherit;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.seg button+button{border-left:1px solid #efede8}.seg button.on{background:#1b1a17;color:#fff}.set-label{font-size:12px;font-weight:600;color:#57534a;margin-bottom:10px}.set-options{display:flex;flex-direction:column;gap:8px;margin-bottom:20px}.set-option{display:flex;align-items:center;gap:9px;font-size:13px;cursor:pointer}.set-option input{accent-color:#1b1a17}.set-scheme{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#a39d90;margin-left:auto}.set-input{font-family:ui-monospace,Menlo,monospace;font-size:12px;padding:8px 10px;border:1px solid #ddd8ce;border-radius:8px;outline:none;margin-left:24px}.set-input:focus{border-color:#1b1a17}.pbtn.primary{background:#1b1a17;border-color:#1b1a17;color:#fff}.pbtn.primary:hover{background:#3a372f;border-color:#3a372f}.sec-h{display:flex;align-items:center;gap:8px;margin:14px 0 6px;position:sticky;top:0;z-index:5;background:#faf9f7;padding:10px 0 8px}.sec-h .sq{width:8px;height:8px;border-radius:2px;flex:none}.sec-h .lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#57534a}.sec-h .n{font-size:11px;color:#a39d90}.sec-h .ln{flex:1;height:1px;background:#e8e4dc}.sec-h.sm{margin:8px 0 4px;padding:8px 0 6px}.sec-h.sm .lbl{font-size:10.5px}.grid-pad{padding:6px 32px 32px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:18px 20px;display:flex;flex-direction:column;gap:10px;cursor:pointer;text-align:left;min-width:0}.card:hover{border-color:#b9b2a3;box-shadow:0 2px 8px #1b1a170f}.card .top{display:flex;align-items:baseline;gap:8px}.card .nm{font-size:15px;font-weight:600;color:#1b1a17;word-break:break-all}.card .ver{margin-left:auto;font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;white-space:nowrap}.card .desc{font-size:13px;line-height:1.65;color:#57534a;margin:0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.card .usage{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;background:#f4f1ea;border-radius:6px;padding:8px 10px;color:#6b6558;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .meta{display:flex;gap:8px;font-size:10.5px;color:#8a8579}.card .meta span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card .meta .meta-r{margin-left:auto;flex:none;display:inline-flex;gap:8px;align-items:center}.scope-mini{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:600;letter-spacing:.04em;color:#8a8579;margin-bottom:-4px}.dot5{width:6px;height:6px;border-radius:2px;flex:none}.empty{text-align:center;padding:44px;font-size:13px;color:#8a8579}.ai-mark{color:#b0836a;font-size:11px}.kbadge{font-size:9.5px;font-weight:700;padding:2px 8px;border-radius:999px;background:#f1efe9;color:#8a8579;white-space:nowrap;flex:none;vertical-align:3px;margin-left:8px}.same-actions{margin-left:auto;display:inline-flex;gap:6px}.pbtn.sm{font-size:11px;padding:3px 10px}.pbtn.sm.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.diff-wrap{margin-top:10px}.diff-legend{display:flex;gap:14px;align-items:center;font-size:11px;margin-bottom:6px}.d-del-mark{color:#b0392e}.d-add-mark{color:#1f8a5b}.d-count{color:#8a8579;margin-left:auto}.diff{border:1px solid #e8e4dc;border-radius:8px;overflow-x:auto;background:#fff;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.7;max-height:480px;overflow-y:auto}.d-line{padding:0 12px;white-space:pre}.d-add{background:#1f8a5b14;color:#16603f}.d-del{background:#b0392e12;color:#8c2d24}.d-skip{padding:2px 12px;color:#a39d90;background:#faf9f7;text-align:center;border-top:1px solid #efede8;border-bottom:1px solid #efede8}.inv-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none}.inv-human{color:#1f8a5b;background:#1f8a5b1a}.inv-agent{color:#7b5bd6;background:#7b5bd61a}.inv-both{color:#b8862f;background:#c07a1f21}.inv-ai{opacity:.85}.rel-chips{display:flex;flex-wrap:wrap;gap:8px}.rel-chip{display:inline-flex;align-items:center;gap:7px;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:7px 12px;cursor:pointer}.rel-chip:hover{background:#f4f1ea}.rel-chip .rt{font-size:9.5px;font-weight:700;text-transform:uppercase;color:#8a8579}.rel-chip .rn{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#1b1a17}.rel-chip.missing{border-style:dashed;cursor:default}.rel-chip.missing .rn{color:#a39d90}.rel-chip.missing .rm{font-size:9.5px;color:#b0836a}.md-wrap{display:flex;align-items:stretch}.left-col{width:340px;flex:none;border-right:1px solid #eeeae2;padding:8px 20px 28px 32px;position:sticky;top:0;max-height:100vh;overflow-y:auto;overscroll-behavior:contain}.ccard{display:flex;flex-direction:column;gap:4px;width:100%;background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:12px 14px;cursor:pointer;text-align:left;margin-bottom:8px}.ccard:hover{border-color:#b9b2a3}.ccard.sel{border-color:#1b1a17;box-shadow:0 1px 5px #1b1a171a}.ccard .r1{display:flex;align-items:center;gap:7px}.ccard .dot7{width:7px;height:7px;border-radius:50%;flex:none}.ccard .nm{font-size:13.5px;font-weight:600;color:#1b1a17;word-break:break-all}.ccard .ver{margin-left:auto;font-size:10px;color:#a39d90;font-family:ui-monospace,Menlo,monospace}.ccard .d1{font-size:11.5px;color:#8a8579;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pane{flex:1;min-width:0;max-width:980px;padding:20px 36px 36px 32px}.back{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:7px 14px;cursor:pointer}.back:hover{background:#f4f1ea}.meta-row{display:flex;align-items:center;gap:10px;margin-top:20px;position:relative;flex-wrap:wrap}.badge{font-size:10.5px;font-weight:700;padding:4px 11px;border-radius:999px}.m-ver{font-size:11px;color:#8a8579;font-family:ui-monospace,Menlo,monospace}.m-upd{margin-left:auto;font-size:11px;color:#8a8579}.pbtn{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.pbtn:hover{background:#f4f1ea}.pbtn.danger{color:#b0392e;border-color:#e4ccc7}.pbtn.danger:hover{background:#fbf1ef}.d-name{font-size:26px;font-weight:700;color:#1b1a17;margin:14px 0 0;word-break:break-all}.tabs{display:flex;gap:20px;margin-top:18px;border-bottom:1px solid #eeeae2}.tab{background:none;border:0;font-size:13px;color:#8a8579;padding:8px 2px 10px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.tab.on{color:#1b1a17;font-weight:600;border-bottom-color:#1b1a17}.sec-t{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;margin:26px 0 10px}.full-desc{font-size:13.5px;line-height:1.8;color:#57534a;margin:0;white-space:pre-wrap}.ex-block{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#f4f1ea;border-radius:8px;padding:10px 13px;color:#57534a;margin-bottom:8px;overflow-x:auto;white-space:pre}.f-row{display:flex;align-items:center;gap:9px;border-bottom:1px solid #eeeae2;padding:8px 4px}.f-row .sq6{width:6px;height:6px;border-radius:2px;background:#c5bfb2;flex:none}.f-row .p{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#57534a;word-break:break-all}.fm-box{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#8a8579;background:#faf9f7;border:1px solid #e8e4dc;border-radius:8px;padding:12px 14px;white-space:pre-wrap;word-break:break-word;margin:18px 0}.md-body{font-size:12.5px;line-height:1.9;color:#3a372f}.md-body h1,.md-body h2{border-bottom:1px solid #eeeae2;padding-bottom:6px}.md-body h1{font-size:19px;margin:26px 0 10px;color:#1b1a17}.md-body h2{font-size:16px;margin:24px 0 10px;color:#1b1a17}.md-body h3{font-size:14px;margin:20px 0 8px;color:#1b1a17}.md-body h4{font-size:13px;margin:16px 0 6px;color:#1b1a17}.md-body p{margin:8px 0}.md-body ul,.md-body ol{margin:8px 0;padding-left:22px}.md-body li{margin:3px 0}.md-body pre{background:#f4f1ea;border-radius:8px;padding:12px 14px;overflow-x:auto;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.6}.md-body code{font-family:ui-monospace,Menlo,monospace;font-size:.9em;background:#f4f1ea;color:#8a5a2b;border-radius:4px;padding:1px 5px}.md-body pre code{background:none;color:inherit;padding:0}.md-body blockquote{border-left:3px solid #ddd8ce;margin:8px 0;padding:2px 0 2px 14px;color:#6b6558}.md-body hr{border:0;border-top:1px solid #eeeae2;margin:18px 0}.md-body a{color:#2a6fdb}.drop{position:absolute;top:calc(100% + 6px);right:0;width:260px;background:#fff;border:1px solid #ddd8ce;border-radius:10px;box-shadow:0 10px 28px #1b1a1724;z-index:30;padding:8px 0;max-height:320px;overflow-y:auto}.drop .dh{font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;padding:6px 14px 4px}.drop .di{display:block;width:100%;text-align:left;background:none;border:0;padding:8px 14px;cursor:pointer}.drop .di:hover{background:#f4f1ea}.drop .di .l1{font-size:12.5px;font-weight:600;color:#1b1a17}.drop .di .l2{font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;word-break:break-all}.overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#1b1a1759;z-index:40;display:flex;align-items:center;justify-content:center}.modal{width:390px;max-width:90vw;background:#fff;border-radius:14px;padding:26px 28px}.modal h3{font-size:15px;font-weight:700;margin:0 0 10px;color:#1b1a17}.modal p{font-size:12.5px;line-height:1.7;color:#57534a;margin:0 0 20px}.modal .btns{display:flex;justify-content:flex-end;gap:8px}.modal .del{background:#b0392e;border:1px solid #b0392e;color:#fff;border-radius:999px;font-size:12px;padding:7px 16px;cursor:pointer}.modal .del:hover{background:#962f27}@media(max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.left-col{display:none}}@media(min-width:1480px){.grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1960px){.grid{grid-template-columns:repeat(5,minmax(0,1fr))}}.unused-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none;color:#5b7285;background:#5b72851f}.warn-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none;color:#b0432f;background:#c848301a;position:relative;cursor:help}.card .top .top-r{margin-left:auto;display:inline-flex;align-items:center;gap:8px;flex:none}.card .top .top-r .ver{margin-left:0}.card .chips{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:-4px}.card .chips .kbadge{margin-left:0;vertical-align:0}.warn-badge .tip{display:none;position:absolute;top:calc(100% + 6px);right:-6px;z-index:40;width:250px;padding:10px 12px;border-radius:8px;background:#1b1a17;color:#f4f1ea;font-size:11.5px;font-weight:400;line-height:1.6;white-space:normal;text-align:left;box-shadow:0 4px 14px #1b1a1740}.warn-badge:hover .tip{display:block}.warn-badge .tip .tip-t{display:block;font-weight:700;margin-bottom:4px}.warn-badge .tip .tip-line{display:block;margin-top:3px;padding-left:12px;text-indent:-12px}.warn-badge .tip .tip-line:before{content:"· "}.card .meta .tok,.tok-total{font-family:ui-monospace,Menlo,monospace;white-space:nowrap}.hd .tok-total{cursor:help}.sec-h .sec-tok{font-size:11px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;cursor:help}.lint-row{display:flex;gap:8px;align-items:baseline;font-size:13px;line-height:1.65;color:#57534a;padding:2px 0}.lint-row .lint-mark{color:#b0432f;flex:none}.chg-banner{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 0 -4px;padding:10px 14px;border:1px solid #e3ddd0;border-radius:10px;background:#faf7ef;font-size:12px;color:#57534a}.chg-title{font-weight:700;flex:none}.chg-group{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.chg-label{font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap}.chg-label.add{color:#1f8a5b;background:#1f8a5b1a}.chg-label.upd{color:#2a6fdb;background:#2a6fdb1a}.chg-label.del{color:#b0432f;background:#c848301a}.chg-chip{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;padding:2px 8px;border-radius:6px;border:1px solid #ddd8ce;background:#fff;color:#57534a;cursor:pointer}.chg-chip:hover{border-color:#b9b2a3}.chg-chip.gone{cursor:default;text-decoration:line-through;color:#a39d90}.chg-more{font-size:11px;color:#8a8579}.chg-ack{margin-left:auto;flex:none}.spark-wrap{display:flex;align-items:flex-end;gap:10px;margin:2px 0 6px}.spark rect{fill:#e3ddd0}.spark rect.on{fill:#1f8a5b}.spark-label{font-size:10.5px;color:#8a8579;white-space:nowrap}.edit-bar{display:flex;justify-content:flex-end;gap:8px;margin:10px 0 12px}.md-editor{width:100%;min-height:480px;padding:14px 16px;border:1px solid #ddd8ce;border-radius:10px;background:#fff;color:#1b1a17;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:12.5px;line-height:1.7;resize:vertical;outline:none;box-sizing:border-box}.md-editor:focus{border-color:#1b1a17}.diag-block{margin:8px 0 4px}.diag-box{border:1px solid #e8e4dc;border-radius:10px;background:#fff;padding:12px 14px;margin-bottom:10px}.diag-verdict{font-size:12.5px;font-weight:700;margin-bottom:4px}.diag-verdict.good{color:#1f8a5b}.diag-verdict.weak{color:#b8862f}.diag-imp-t{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:#8a8579;margin:10px 0 4px}.diag-improved{font-size:13px;line-height:1.65;color:#57534a;margin:0 0 10px;background:#faf7ef;border-radius:8px;padding:10px 12px}