skills-viewer 0.2.0 → 0.4.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
@@ -17,7 +17,11 @@ npx skills-viewer
17
17
  ## Features
18
18
 
19
19
  - **All scopes in one view** — user (`~/.claude/skills`), every project's `.claude/skills` / `.claude/commands`, installed plugins, and Claude Code built-ins, grouped by source
20
- - **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, or updated date
20
+ - **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, updated date, or token cost
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
+ - **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
+ - **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
+ - **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
21
25
  - **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
22
26
  - **AI summaries** — one-click summarization of each SKILL.md via `claude -p --model haiku`, cached by content hash in `~/.cache/skills-viewer/` so unchanged skills are never re-summarized
23
27
  - **Detail pane** — master/detail layout with an Overview tab (description, usage, bundled files) and a rendered SKILL.md tab
@@ -68,7 +72,8 @@ dist/ # prebuilt UI shipped in the npm package (generated by prepack
68
72
 
69
73
  ## Notes
70
74
 
71
- - Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days)
75
+ - Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days) — the _unused_ badge has the same limitation
76
+ - Token costs are heuristic estimates (≈4 chars/token for ASCII, ≈1.5 for CJK), not exact tokenizer counts
72
77
  - Usage is attributed per calling project (worktrees roll up to their parent project by encoded-path prefix). When scopes share a name, the resolution order project > user > plugin > built-in is assumed
73
78
  - The built-in skill list is hardcoded in `server/scan.js` (they live inside the Claude Code binary); check `/skills` inside Claude Code for the authoritative list
74
79
  - AI summaries require a logged-in `claude` CLI
@@ -48,6 +48,7 @@ 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 snapshot_1 = require("./snapshot");
51
52
  const errors_1 = require("./errors");
52
53
  const locale_1 = require("./locale");
53
54
  const TOKEN = crypto.randomBytes(16).toString('hex');
@@ -67,6 +68,7 @@ const MIME = {
67
68
  * 呼び出し元プロジェクト(トランスクリプトのディレクトリ)を特定し、そのプロジェクトの
68
69
  * skill → user → plugin → built-in の解決優先順で1つの定義にだけ加算する。
69
70
  * 呼び出し元が未知(worktree・削除済みプロジェクト等)の場合は user 以降にフォールバック。
71
+ * 戻り値はトランスクリプトが1件でも存在したか(false なら「未使用」判定は無意味)。
70
72
  */
71
73
  function attributeUsage(sections) {
72
74
  const byDir = (0, usage_1.scanUsageByDir)();
@@ -119,12 +121,18 @@ function attributeUsage(sections) {
119
121
  target.autoCount = (target.autoCount || 0) + u.auto;
120
122
  target.useCount = (target.useCount || 0) + u.typed + u.auto;
121
123
  target.lastUsed = Math.max(target.lastUsed || 0, u.last);
124
+ if (u.daily) {
125
+ const du = target.dailyUse || (target.dailyUse = {});
126
+ for (const [day, n] of Object.entries(u.daily))
127
+ du[day] = (du[day] || 0) + n;
128
+ }
122
129
  }
123
130
  }
131
+ return Object.keys(byDir).length > 0;
124
132
  }
125
133
  function collect(cwd, lang) {
126
134
  const sections = (0, scan_1.scanSections)(cwd, lang);
127
- attributeUsage(sections);
135
+ const usageAvailable = attributeUsage(sections);
128
136
  const summaries = (0, summary_1.loadSummaries)();
129
137
  for (const sec of sections) {
130
138
  for (const it of sec.items) {
@@ -153,7 +161,15 @@ function collect(cwd, lang) {
153
161
  .sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
154
162
  .map((p) => ({ label: path.basename(p), sub: p, path: p })),
155
163
  ];
156
- return { generatedAt: new Date().toISOString(), cwd, sections, targets, aiStale };
164
+ return {
165
+ generatedAt: new Date().toISOString(),
166
+ cwd,
167
+ sections,
168
+ targets,
169
+ aiStale,
170
+ usageAvailable,
171
+ changes: (0, snapshot_1.computeChanges)(sections),
172
+ };
157
173
  }
158
174
  /* DNS rebinding 対策: same-origin GET には Origin が付かないため Host 側も検証する */
159
175
  function hostOk(req) {
@@ -222,6 +238,10 @@ function handleApi(req, res, cwd) {
222
238
  }
223
239
  const lang = langOf(data.lang);
224
240
  try {
241
+ if (url.pathname === '/api/changes-ack') {
242
+ (0, snapshot_1.ackChanges)((0, scan_1.scanSections)(cwd, lang));
243
+ return send(200, { ok: true });
244
+ }
225
245
  if (url.pathname === '/api/copy')
226
246
  return send(200, (0, manage_1.doCopy)(data, cwd));
227
247
  if (url.pathname === '/api/delete')
@@ -269,6 +289,33 @@ function serveStatic(req, res) {
269
289
  res.writeHead(200, { 'Content-Type': MIME[path.extname(fp)] || 'application/octet-stream' });
270
290
  res.end(fs.readFileSync(fp));
271
291
  }
292
+ /*
293
+ * 起動時の1〜2行サマリー(--no-open 運用でも価値が出るように)。
294
+ * 前回からの差分 + セッション注入トークン概算 + 未使用件数。失敗しても起動は止めない。
295
+ */
296
+ function printStartupSummary(cwd) {
297
+ try {
298
+ const data = collect(cwd, locale_1.serverLang);
299
+ const ch = data.changes;
300
+ if (ch) {
301
+ console.log((0, locale_1.srvMsg)(`前回から: 追加 ${ch.added.length} / 更新 ${ch.updated.length} / 削除 ${ch.removed.length}`, `Since last run: ${ch.added.length} added / ${ch.updated.length} updated / ${ch.removed.length} removed`));
302
+ }
303
+ const sessionTokens = data.sections
304
+ .filter((s) => s.source !== 'project' || s.isCurrent)
305
+ .flatMap((s) => s.items)
306
+ .reduce((sum, it) => sum + (it.tokens || 0), 0);
307
+ const unused = data.usageAvailable
308
+ ? data.sections.flatMap((s) => s.items).filter((it) => it.kind !== 'hook' && !it.useCount)
309
+ .length
310
+ : null;
311
+ console.log((0, locale_1.srvMsg)(`スキル定義のセッション注入 ≈${sessionTokens.toLocaleString()}tok` +
312
+ (unused !== null ? ` / 未使用 ${unused} 件` : ''), `Skill definitions inject ≈${sessionTokens.toLocaleString()} tok/session` +
313
+ (unused !== null ? ` / ${unused} unused` : '')));
314
+ }
315
+ catch {
316
+ /* サマリーは補助情報。失敗しても起動を妨げない */
317
+ }
318
+ }
272
319
  function start({ port = 4763, open = true, cwd = process.cwd() } = {}, attempt = 0) {
273
320
  const server = http.createServer((req, res) => {
274
321
  if ((req.url || '').startsWith('/api/'))
@@ -287,6 +334,7 @@ function start({ port = 4763, open = true, cwd = process.cwd() } = {}, attempt =
287
334
  server.listen(port, '127.0.0.1', () => {
288
335
  const url = `http://127.0.0.1:${port}`;
289
336
  console.log(`skills-viewer: ${url} (cwd: ${cwd})`);
337
+ printStartupSummary(cwd);
290
338
  console.log((0, locale_1.srvMsg)('Ctrl+C で終了。ページ再読み込みで再スキャンされます。', 'Press Ctrl+C to quit. Reloading the page rescans.'));
291
339
  if (open && process.platform === 'darwin')
292
340
  (0, node_child_process_1.execFile)('open', [url], () => { });
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /*
3
+ * description の静的リントと、セッション注入トークン量の概算。
4
+ * Claude Code はスキル選択のために frontmatter の name + description を毎セッション
5
+ * コンテキストへ注入する。lint は「モデルが発動判断に使える description か」の観点で、
6
+ * frontmatter の meta(本文フォールバック前)に対して行う。
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.estimateTokens = estimateTokens;
10
+ exports.lintItem = lintItem;
11
+ /*
12
+ * トークン数の概算(厳密なトークナイザは依存が重いので入れない)。
13
+ * 目安: ASCII ≈ 4 文字/トークン、非 ASCII(日本語等)≈ 1.5 文字/トークン。
14
+ */
15
+ function estimateTokens(text) {
16
+ let ascii = 0;
17
+ let other = 0;
18
+ for (const ch of text) {
19
+ if (ch.charCodeAt(0) < 128)
20
+ ascii++;
21
+ else
22
+ other++;
23
+ }
24
+ return Math.ceil(ascii / 4 + other / 1.5);
25
+ }
26
+ /*
27
+ * 発動条件らしい語句。en は "use when/for …"・条件節、ja は「〜のときに使用」系。
28
+ * command は人間が明示的に打つ起点なので発動条件は要求しない(呼び出し側で除外)。
29
+ */
30
+ const TRIGGER_RE = /\b(use|when|whenever|if|for|before|after|trigger|invoke)\b|使用|使う|使って|用い|とき|時に|場合|際|なら|向け/i;
31
+ const SHORT_LIMIT = 30;
32
+ const LONG_LIMIT = 1024;
33
+ /*
34
+ * meta: frontmatter のパース結果。name はフォールバック解決後の表示名。
35
+ * 戻り値は言語非依存のコード(表示ラベルは web の i18n で解決)。
36
+ */
37
+ function lintItem(meta, name, kind) {
38
+ if (kind === 'hook')
39
+ return [];
40
+ const warnings = [];
41
+ const desc = (meta.description || '').trim();
42
+ if (!desc)
43
+ return ['no-description'];
44
+ if (desc.length < SHORT_LIMIT)
45
+ warnings.push('short-description');
46
+ if (desc.length > LONG_LIMIT)
47
+ warnings.push('long-description');
48
+ // 記号を除いて比較し、name をほぼ繰り返しただけの description を検出
49
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9ぁ-んァ-ン一-龠]/g, '');
50
+ if (norm(desc) === norm(name))
51
+ warnings.push('name-echo');
52
+ // skill / agent はモデルが description を見て自動発動するため、発動条件の記述を求める
53
+ if ((kind === 'skill' || kind === 'agent') && !TRIGGER_RE.test(desc)) {
54
+ warnings.push('no-trigger');
55
+ }
56
+ return warnings;
57
+ }
@@ -41,6 +41,7 @@ exports.scanSections = scanSections;
41
41
  const fs = __importStar(require("node:fs"));
42
42
  const os = __importStar(require("node:os"));
43
43
  const path = __importStar(require("node:path"));
44
+ const lint_1 = require("./lint");
44
45
  exports.HOME = os.homedir();
45
46
  /* ---------- frontmatter parsing (minimal YAML: scalars + block scalars) ---------- */
46
47
  function parseFrontmatter(raw) {
@@ -129,8 +130,10 @@ function readSkillDir(dir, nameHint) {
129
130
  return null; // 権限エラー等で読めない skill はスキップ(一覧全体を落とさない)
130
131
  }
131
132
  const { meta, body } = parseFrontmatter(raw);
133
+ const name = meta.name || nameHint;
134
+ const lint = (0, lint_1.lintItem)(meta, name, 'skill');
132
135
  return {
133
- name: meta.name || nameHint,
136
+ name,
134
137
  description: meta.description || firstBodyLine(body),
135
138
  argumentHint: meta['argument-hint'] || '',
136
139
  version: meta.version || '',
@@ -138,6 +141,7 @@ function readSkillDir(dir, nameHint) {
138
141
  path: skillMd,
139
142
  updatedAt: fileMtime(skillMd),
140
143
  files: listFiles(dir).sort(),
144
+ ...(lint.length ? { lint } : {}),
141
145
  _body: body, // 参照抽出用(scanSections で refs 化して破棄)
142
146
  };
143
147
  }
@@ -171,8 +175,10 @@ function scanMdRoot(root, kind) {
171
175
  continue; // 読めないファイルはスキップ(一覧全体を落とさない)
172
176
  }
173
177
  const { meta, body } = parseFrontmatter(raw);
178
+ const name = meta.name || entry.name.replace(/\.md$/, '');
179
+ const lint = (0, lint_1.lintItem)(meta, name, kind);
174
180
  items.push({
175
- name: meta.name || entry.name.replace(/\.md$/, ''),
181
+ name,
176
182
  description: meta.description || firstBodyLine(body),
177
183
  argumentHint: kind === 'command' ? meta['argument-hint'] || '' : '',
178
184
  version: meta.version || '',
@@ -180,6 +186,7 @@ function scanMdRoot(root, kind) {
180
186
  path: fp,
181
187
  updatedAt: fileMtime(fp),
182
188
  files: [entry.name],
189
+ ...(lint.length ? { lint } : {}),
183
190
  _body: body,
184
191
  });
185
192
  }
@@ -440,5 +447,13 @@ function scanSections(cwd, lang = 'en') {
440
447
  },
441
448
  ];
442
449
  attachRefs(sections);
450
+ // name + description は毎セッション注入されるため、その分のトークンを概算しておく。
451
+ // hook は設定エントリ(description 注入なし)なので対象外
452
+ for (const s of sections) {
453
+ for (const it of s.items) {
454
+ if (it.kind !== 'hook')
455
+ it.tokens = (0, lint_1.estimateTokens)(it.name + ': ' + it.description);
456
+ }
457
+ }
443
458
  return sections;
444
459
  }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /*
3
+ * 前回起動時のアイテム一覧スナップショット(~/.cache/skills-viewer/snapshot.json)と
4
+ * 現在のスキャン結果との diff(追加・更新・削除)。「前回から何が変わったか」の基盤。
5
+ * - hook は同一 path・同一 name の複数定義があり識別子が安定しないため対象外
6
+ * - built-in は実ファイルが無く description が表示言語依存のため対象外(path='' で除外される)
7
+ * スナップショットの更新はユーザーの「既読」操作時のみ(差分バナーはリロードしても消えない)。
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.buildSnapshot = buildSnapshot;
44
+ exports.diffSnapshot = diffSnapshot;
45
+ exports.computeChanges = computeChanges;
46
+ exports.ackChanges = ackChanges;
47
+ const fs = __importStar(require("node:fs"));
48
+ const os = __importStar(require("node:os"));
49
+ const path = __importStar(require("node:path"));
50
+ const summary_1 = require("./summary");
51
+ const SNAPSHOT_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'snapshot.json');
52
+ function buildSnapshot(sections) {
53
+ const snap = {};
54
+ for (const s of sections) {
55
+ for (const it of s.items) {
56
+ if (it.kind === 'hook' || !it.path)
57
+ continue;
58
+ snap[it.path] = { name: it.name, kind: it.kind, hash: (0, summary_1.contentHash)(it.path) };
59
+ }
60
+ }
61
+ return snap;
62
+ }
63
+ function loadSnapshot() {
64
+ try {
65
+ return JSON.parse(fs.readFileSync(SNAPSHOT_FILE, 'utf8'));
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function saveSnapshot(snap) {
72
+ try {
73
+ fs.mkdirSync(path.dirname(SNAPSHOT_FILE), { recursive: true });
74
+ fs.writeFileSync(SNAPSHOT_FILE, JSON.stringify(snap, null, 1));
75
+ }
76
+ catch {
77
+ /* キャッシュが書けなくても本体機能には影響させない */
78
+ }
79
+ }
80
+ function diffSnapshot(prev, cur) {
81
+ const added = [];
82
+ const updated = [];
83
+ const removed = [];
84
+ for (const [p, e] of Object.entries(cur)) {
85
+ const old = prev[p];
86
+ if (!old)
87
+ added.push({ name: e.name, kind: e.kind, path: p });
88
+ else if (old.hash !== e.hash)
89
+ updated.push({ name: e.name, kind: e.kind, path: p });
90
+ }
91
+ for (const [p, e] of Object.entries(prev)) {
92
+ if (!cur[p])
93
+ removed.push({ name: e.name, kind: e.kind, path: p });
94
+ }
95
+ return { added, updated, removed };
96
+ }
97
+ /*
98
+ * 現在のスキャン結果と前回スナップショットの差分。
99
+ * 初回(スナップショット無し)は全件が「追加」になってしまうため、基準だけ保存して null。
100
+ */
101
+ function computeChanges(sections) {
102
+ const cur = buildSnapshot(sections);
103
+ const prev = loadSnapshot();
104
+ if (!prev) {
105
+ saveSnapshot(cur);
106
+ return null;
107
+ }
108
+ const d = diffSnapshot(prev, cur);
109
+ return d.added.length || d.updated.length || d.removed.length ? d : null;
110
+ }
111
+ /* 「既読にする」: 現在の状態を新しい基準として保存する */
112
+ function ackChanges(sections) {
113
+ saveSnapshot(buildSnapshot(sections));
114
+ }
@@ -41,6 +41,7 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  };
42
42
  })();
43
43
  Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.dayKey = dayKey;
44
45
  exports.extractHits = extractHits;
45
46
  exports.scanUsageByDir = scanUsageByDir;
46
47
  exports.encodeProjectPath = encodeProjectPath;
@@ -48,6 +49,12 @@ const fs = __importStar(require("node:fs"));
48
49
  const os = __importStar(require("node:os"));
49
50
  const path = __importStar(require("node:path"));
50
51
  const node_string_decoder_1 = require("node:string_decoder");
52
+ /* ローカルタイムゾーンの日付キー。web 側のスパークラインと同じ形式であること */
53
+ function dayKey(ts) {
54
+ const d = new Date(ts);
55
+ const p = (n) => String(n).padStart(2, '0');
56
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
57
+ }
51
58
  const usageCache = new Map();
52
59
  function scanLine(line, hits) {
53
60
  const isCmd = line.includes('<command-name>');
@@ -150,10 +157,14 @@ function scanUsageByDir() {
150
157
  usageCache.set(fp, entry);
151
158
  }
152
159
  for (const h of entry.hits) {
153
- const a = agg[h.name] || (agg[h.name] = { typed: 0, auto: 0, last: 0 });
160
+ const a = agg[h.name] || (agg[h.name] = { typed: 0, auto: 0, last: 0, daily: {} });
154
161
  a[h.via === 'typed' ? 'typed' : 'auto']++;
155
162
  if (h.ts > a.last)
156
163
  a.last = h.ts;
164
+ if (h.ts > 0) {
165
+ const day = dayKey(h.ts);
166
+ a.daily[day] = (a.daily[day] || 0) + 1;
167
+ }
157
168
  }
158
169
  }
159
170
  }
@@ -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))}}
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}