@musnows/scriverse 0.5.5 → 0.5.7

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.
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260727-inline-save-spacing-v2">
13
+ <link rel="stylesheet" href="/styles.css?v=20260728-writing-daily-line-v2">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -140,6 +140,7 @@
140
140
  <span>作品目录</span>
141
141
  <div class="panel-heading-actions">
142
142
  <span id="chapter-count">0 章</span>
143
+ <button id="chapter-batch-button" class="chapter-batch-button" type="button" aria-label="批量管理章节" title="批量管理章节"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m3.5 6 1.5 1.5L7.5 5"/><path d="M10 6h10"/><path d="m3.5 12 1.5 1.5 2.5-2.5"/><path d="M10 12h10"/><path d="m3.5 18 1.5 1.5 2.5-2.5"/><path d="M10 18h10"/></svg></button>
143
144
  <button id="new-volume-button" class="add-button" type="button" aria-label="新建分卷" title="新建分卷">+</button>
144
145
  </div>
145
146
  </div>
@@ -193,8 +194,10 @@
193
194
  <button id="user-management-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">人</span><strong>用户管理</strong><small>管理员、普通用户与账户状态</small></button>
194
195
  <button id="platform-ui-settings-button" class="settings-hub-card hidden" type="button"><span class="settings-card-mark">界</span><strong>界面与分页</strong><small>设置通知位置及各模块的单页数量</small></button>
195
196
  <button id="collaboration-button" class="settings-hub-card" type="button"><span class="settings-card-mark">协</span><strong>作品协作</strong><small>邀请注册用户共同编辑当前作品</small></button>
197
+ <button id="writing-progress-button" class="settings-hub-card" type="button"><span class="settings-card-mark">字</span><strong>写作目标</strong><small>每日目标、总字数目标与近 30 天趋势</small></button>
198
+ <button id="work-audit-button" class="settings-hub-card" type="button"><span class="settings-card-mark">录</span><strong>操作记录</strong><small>查看作品修改、操作者、对象与时间</small></button>
196
199
  <button id="appearance-button" class="settings-hub-card" type="button" data-settings-action="appearance"><span class="settings-card-mark">Aa</span><strong>显示设置</strong><small>中文字体、等宽英文字体、字号与行距</small></button>
197
- <button id="export-button" class="settings-hub-card" type="button" data-settings-action="export"><span class="settings-card-mark">MD</span><strong>导出作品</strong><small>将当前作品安全导出为 Markdown</small></button>
200
+ <button id="export-button" class="settings-hub-card" type="button" data-settings-action="export"><span class="settings-card-mark">ZIP</span><strong>导出正文</strong><small>以 ZIP 下载 Markdown 正文,不包含角色和设定资料</small></button>
198
201
  </div>
199
202
  <p id="settings-work-note" class="settings-work-note"></p>
200
203
  <footer class="product-footer settings-product-footer" data-product-footer aria-label="叙界项目信息">
@@ -223,6 +226,8 @@
223
226
  <span id="chapter-stats" class="chapter-stats">0 字 · v0</span>
224
227
  <button id="insight-button" class="ghost-button" type="button" aria-controls="chapter-insight-toast" aria-expanded="false">章节概览</button>
225
228
  <button id="versions-button" class="ghost-button" type="button">版本</button>
229
+ <button id="chapter-annotations-button" class="ghost-button" type="button">批注与待办</button>
230
+ <button id="chapter-delete-button" class="danger-button" type="button">删除章节</button>
226
231
  <button id="chapter-edit-button" class="primary-button hidden" type="button">编辑</button>
227
232
  <button id="tidy-blank-lines-button" class="ghost-button" type="button">整理空行</button>
228
233
  <button id="save-button" class="primary-button" type="button">保存正文</button>
@@ -312,18 +317,21 @@
312
317
  </div>
313
318
 
314
319
  <div id="toast-region" class="toast-region" data-position="bottom-right" aria-live="polite" popover="manual"></div>
315
- <div id="chapter-type-menu" class="chapter-type-menu hidden" role="menu" aria-label="设置章节类型" data-testid="chapter-type-menu">
320
+ <div id="chapter-type-menu" class="chapter-type-menu hidden" role="menu" aria-label="章节操作" data-testid="chapter-type-menu">
316
321
  <strong>标记章节</strong>
317
322
  <div>
318
323
  <button type="button" role="menuitemradio" data-chapter-type="正文">正文</button>
319
324
  <button type="button" role="menuitemradio" data-chapter-type="设定">设定</button>
320
325
  <button type="button" role="menuitemradio" data-chapter-type="作者的话">作者的话</button>
321
326
  <button type="button" role="menuitemradio" data-chapter-type="其他">其他</button>
327
+ <button class="danger-button" type="button" role="menuitem" data-delete-chapter>删除章节</button>
322
328
  </div>
323
329
  </div>
324
330
  <div id="line-citation-menu" class="line-citation-menu hidden" role="menu" aria-label="正文行引用操作">
325
331
  <strong id="line-citation-label">已选择正文</strong>
326
332
  <button id="add-line-citation" type="button" role="menuitem">添加到创作助手引用</button>
333
+ <button id="add-line-annotation" type="button" role="menuitem">添加批注</button>
334
+ <button id="add-line-todo" type="button" role="menuitem">添加待办</button>
327
335
  </div>
328
336
  <input id="new-import-file" class="hidden" type="file" accept=".txt,.docx">
329
337
  <input id="cover-file" class="hidden" type="file" accept="image/png,image/jpeg,image/webp">
@@ -381,6 +389,38 @@
381
389
  </form>
382
390
  </dialog>
383
391
 
392
+ <dialog id="chapter-batch-dialog" class="dialog chapter-batch-dialog" aria-labelledby="chapter-batch-title">
393
+ <form id="chapter-batch-form">
394
+ <div class="dialog-header">
395
+ <div><span class="eyebrow">正文工具</span><h2 id="chapter-batch-title">章节批量管理</h2><p class="dialog-header-meta">选择多个章节后统一移动、修改属性或软删除。</p></div>
396
+ <button id="chapter-batch-close" class="dialog-close" type="button" aria-label="关闭章节批量管理">×</button>
397
+ </div>
398
+ <div class="chapter-batch-toolbar">
399
+ <button id="chapter-batch-select-all" class="ghost-button" type="button">全选</button>
400
+ <button id="chapter-batch-clear" class="ghost-button" type="button">清空</button>
401
+ <output id="chapter-batch-count">已选择 0 章</output>
402
+ </div>
403
+ <div class="chapter-batch-search">
404
+ <label for="chapter-batch-search">搜索章节名称</label>
405
+ <div class="chapter-batch-search-field">
406
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4 4"/></svg>
407
+ <input id="chapter-batch-search" type="search" placeholder="输入章节名称" autocomplete="off" aria-describedby="chapter-batch-search-count">
408
+ <output id="chapter-batch-search-count">0 章</output>
409
+ </div>
410
+ </div>
411
+ <div id="chapter-batch-list" class="chapter-batch-list"></div>
412
+ <div class="chapter-batch-controls">
413
+ <label>批量操作<select id="chapter-batch-action"><option value="move">移动到分卷</option><option value="setType">修改章节类型</option><option value="exclude">排除 AI 分析</option><option value="include">纳入 AI 分析</option><option value="delete">软删除章节</option></select></label>
414
+ <label id="chapter-batch-volume-field">目标分卷<select id="chapter-batch-volume"></select></label>
415
+ <label id="chapter-batch-type-field" class="hidden">章节类型<select id="chapter-batch-type"><option value="正文">正文</option><option value="设定">设定</option><option value="作者的话">作者的话</option><option value="其他">其他</option></select></label>
416
+ </div>
417
+ <div class="dialog-actions">
418
+ <button id="chapter-batch-cancel" class="ghost-button" type="button">取消</button>
419
+ <button id="chapter-batch-apply" class="primary-button" type="submit" disabled>应用到所选章节</button>
420
+ </div>
421
+ </form>
422
+ </dialog>
423
+
384
424
  <dialog id="import-mode-dialog" class="dialog import-mode-dialog" aria-labelledby="import-mode-dialog-title" aria-describedby="import-mode-dialog-description" data-testid="import-mode-dialog">
385
425
  <form method="dialog">
386
426
  <div class="dialog-header">
@@ -607,6 +647,17 @@
607
647
  </div>
608
648
  </dialog>
609
649
 
650
+ <dialog id="chapter-recycle-bin-dialog" class="dialog wide-dialog" aria-labelledby="chapter-recycle-bin-title" aria-describedby="chapter-recycle-bin-description">
651
+ <div class="dialog-header">
652
+ <div><span class="eyebrow">正文安全</span><h2 id="chapter-recycle-bin-title">章节回收站</h2></div>
653
+ <button id="chapter-recycle-bin-close" class="dialog-close" aria-label="关闭章节回收站" type="button">×</button>
654
+ </div>
655
+ <div class="import-history-body">
656
+ <p id="chapter-recycle-bin-description" class="import-history-note">软删除的章节会保留正文、版本和关联资料。恢复后章节将重新出现在原分卷中。</p>
657
+ <div id="chapter-recycle-bin-list" class="entity-history-list" aria-live="polite"></div>
658
+ </div>
659
+ </dialog>
660
+
610
661
  <dialog id="users-dialog" class="dialog wide-dialog" aria-labelledby="users-dialog-title">
611
662
  <div class="dialog-header"><div><span class="eyebrow">系统管理员</span><h2 id="users-dialog-title">用户管理</h2></div><div class="settings-dialog-header-actions"><button id="users-settings-return" class="ghost-button settings-parent-button" type="button">返回设置</button><button id="users-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div></div>
612
663
  <div class="access-dialog-body">
@@ -614,6 +665,30 @@
614
665
  </div>
615
666
  </dialog>
616
667
 
668
+ <dialog id="chapter-annotations-dialog" class="dialog wide-dialog chapter-annotations-dialog" aria-labelledby="chapter-annotations-title">
669
+ <div class="dialog-header"><div><span class="eyebrow">正文协作</span><h2 id="chapter-annotations-title">批注与待办</h2><p id="chapter-annotations-meta" class="dialog-header-meta">选择正文行后可添加批注或待办。</p></div><button id="chapter-annotations-close" class="dialog-close" aria-label="关闭批注与待办" type="button">×</button></div>
670
+ <div id="chapter-annotations-list" class="chapter-annotations-list"></div>
671
+ <div class="dialog-actions"><button id="chapter-annotations-refresh" class="ghost-button" type="button">刷新</button><button id="chapter-annotations-done" class="primary-button" type="button">完成</button></div>
672
+ </dialog>
673
+
674
+ <dialog id="writing-progress-dialog" class="dialog wide-dialog writing-progress-dialog" aria-labelledby="writing-progress-title">
675
+ <form id="writing-goal-form">
676
+ <div class="dialog-header"><div><span class="eyebrow">创作节奏</span><h2 id="writing-progress-title">写作目标与字数趋势</h2><p class="dialog-header-meta">趋势根据正文版本历史重建,软删除章节不会继续计入。</p></div><button id="writing-progress-close" class="dialog-close" aria-label="关闭写作目标" type="button">×</button></div>
677
+ <div class="writing-progress-body">
678
+ <div class="writing-progress-stats"><article><small>当前总字数</small><strong id="writing-current-words">0</strong></article><article><small>今日新增</small><strong id="writing-today-words">0</strong></article><article><small>每日目标完成</small><strong id="writing-daily-completion">0%</strong></article><article><small>总目标完成</small><strong id="writing-total-completion">0%</strong></article></div>
679
+ <div id="writing-trend-chart" class="writing-trend-chart" role="group" aria-label="近 30 天作品字数趋势"></div>
680
+ <div class="writing-goal-fields"><label>每日目标字数<input id="writing-daily-goal" type="number" min="0" max="1000000" required></label><label>总字数目标<input id="writing-total-goal" type="number" min="0" max="100000000" required></label><label>计划完成日期<input id="writing-deadline" type="date"></label></div>
681
+ </div>
682
+ <div class="dialog-actions"><button id="writing-progress-refresh" class="ghost-button" type="button">刷新趋势</button><button id="writing-goal-save" class="primary-button" type="submit">保存目标</button></div>
683
+ </form>
684
+ </dialog>
685
+
686
+ <dialog id="work-audit-dialog" class="dialog wide-dialog work-audit-dialog" aria-labelledby="work-audit-title">
687
+ <div class="dialog-header"><div><span class="eyebrow">作品安全</span><h2 id="work-audit-title">操作记录</h2><p class="dialog-header-meta">按时间倒序展示当前作品的写入与管理操作。</p></div><div class="settings-dialog-header-actions"><button id="work-audit-settings-return" class="ghost-button settings-parent-button" type="button">返回设置</button><button id="work-audit-close" class="dialog-close" aria-label="关闭操作记录" type="button">×</button></div></div>
688
+ <div id="work-audit-list" class="work-audit-list"></div>
689
+ <div class="dialog-actions"><button id="work-audit-refresh" class="ghost-button" type="button">刷新</button><button id="work-audit-load-more" class="primary-button hidden" type="button">加载更多</button></div>
690
+ </dialog>
691
+
617
692
  <dialog id="platform-ui-settings-dialog" class="dialog" aria-labelledby="platform-ui-settings-title">
618
693
  <div class="dialog-header"><div><span class="eyebrow">系统管理员</span><h2 id="platform-ui-settings-title">界面与分页</h2></div><div class="settings-dialog-header-actions"><button id="platform-ui-settings-return" class="ghost-button settings-parent-button" type="button">返回设置</button><button id="platform-ui-settings-close" class="dialog-close" aria-label="关闭" type="button">×</button></div></div>
619
694
  <form id="platform-ui-settings-form">
@@ -788,6 +863,6 @@
788
863
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
789
864
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
790
865
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
791
- <script type="module" src="/app.js?v=20260727-race-tree-pagination-v1"></script>
866
+ <script type="module" src="/app.js?v=20260728-galaxy-edge-stars-v3"></script>
792
867
  </body>
793
868
  </html>
@@ -6,7 +6,7 @@ const RELATION_STYLE = Object.freeze({
6
6
  uncertain: { label: "未确定", color: "#9aa5b5" }
7
7
  });
8
8
 
9
- /** Obsidian Graph View 风格:为浅色和深色背景分别提供可辨识的低饱和度分组配色 */
9
+ /** Obsidian Graph View 风格:为浅色和深色背景提供可辨识的低饱和度均衡配色 */
10
10
  export const OBSIDIAN_NODE_PALETTE = Object.freeze([
11
11
  Object.freeze({ key: "blue", color: "#3f6f99", darkColor: "#7fb7e3", glow: "rgba(63,111,153,.42)", darkGlow: "rgba(127,183,227,.56)" }),
12
12
  Object.freeze({ key: "indigo", color: "#5b5fa3", darkColor: "#9ea6ed", glow: "rgba(91,95,163,.42)", darkGlow: "rgba(158,166,237,.56)" }),
@@ -25,7 +25,13 @@ export const OBSIDIAN_NODE_PALETTE = Object.freeze([
25
25
  Object.freeze({ key: "steel", color: "#576f86", darkColor: "#98afc5", glow: "rgba(87,111,134,.42)", darkGlow: "rgba(152,175,197,.56)" }),
26
26
  Object.freeze({ key: "slate", color: "#626b78", darkColor: "#aab3c0", glow: "rgba(98,107,120,.42)", darkGlow: "rgba(170,179,192,.56)" }),
27
27
  Object.freeze({ key: "sand", color: "#806e58", darkColor: "#c5ad91", glow: "rgba(128,110,88,.42)", darkGlow: "rgba(197,173,145,.56)" }),
28
- Object.freeze({ key: "taupe", color: "#75636d", darkColor: "#b9a0ad", glow: "rgba(117,99,109,.42)", darkGlow: "rgba(185,160,173,.56)" })
28
+ Object.freeze({ key: "taupe", color: "#75636d", darkColor: "#b9a0ad", glow: "rgba(117,99,109,.42)", darkGlow: "rgba(185,160,173,.56)" }),
29
+ Object.freeze({ key: "gold", color: "#8b702b", darkColor: "#dfbd63", glow: "rgba(139,112,43,.42)", darkGlow: "rgba(223,189,99,.56)" }),
30
+ Object.freeze({ key: "lime", color: "#5e7a35", darkColor: "#a4cd74", glow: "rgba(94,122,53,.42)", darkGlow: "rgba(164,205,116,.56)" }),
31
+ Object.freeze({ key: "mint", color: "#397d59", darkColor: "#78ce99", glow: "rgba(57,125,89,.42)", darkGlow: "rgba(120,206,153,.56)" }),
32
+ Object.freeze({ key: "turquoise", color: "#287d78", darkColor: "#69cec8", glow: "rgba(40,125,120,.42)", darkGlow: "rgba(105,206,200,.56)" }),
33
+ Object.freeze({ key: "azure", color: "#326ea3", darkColor: "#76b7ee", glow: "rgba(50,110,163,.42)", darkGlow: "rgba(118,183,238,.56)" }),
34
+ Object.freeze({ key: "peach", color: "#a35d50", darkColor: "#eda197", glow: "rgba(163,93,80,.42)", darkGlow: "rgba(237,161,151,.56)" })
29
35
  ]);
30
36
 
31
37
  const clamp = (value, minimum, maximum) => Math.min(maximum, Math.max(minimum, value));
@@ -87,6 +93,8 @@ const GALAXY_CELESTIAL_TYPES = Object.freeze({
87
93
  outer: Object.freeze(["rocky", "ocean", "ice", "volcanic", "dwarf", "ringed"])
88
94
  });
89
95
  export const GALAXY_ROTATION_RADIANS_PER_MS = 0.000012;
96
+ export const GALAXY_BASE_STAR_COUNT = 7200;
97
+ export const GALAXY_EDGE_STAR_BOOST_RATIO = 1.1 * 1.1 - 1;
90
98
  export const GALAXY_LAYOUT_CONFIG = Object.freeze({
91
99
  minimumRadius: 220,
92
100
  radialSpan: 830,
@@ -220,15 +228,9 @@ export function resolveRelationshipNodeGroup(node) {
220
228
  return { type: "default", key: "default", label: "未分组" };
221
229
  }
222
230
 
223
- export function getObsidianNodeAppearance(node, maxDegree = 1) {
224
- const group = resolveRelationshipNodeGroup(node);
231
+ function createObsidianNodeAppearance(node, maxDegree, group, paletteIndex) {
225
232
  const degree = Math.max(0, Number(node?.degree) || 0);
226
233
  const normalizedDegree = clamp(degree / Math.max(1, Number(maxDegree) || 1), 0, 1);
227
- // 未分组角色没有可共享的分组语义,按角色稳定标识散列,避免资料不完整时大片同色。
228
- const colorKey = group.key === "default"
229
- ? `node:${String(node?.id ?? node?.name ?? "default")}`
230
- : group.key;
231
- const paletteIndex = hashString(colorKey) % OBSIDIAN_NODE_PALETTE.length;
232
234
  const palette = OBSIDIAN_NODE_PALETTE[paletteIndex];
233
235
  return {
234
236
  group,
@@ -242,6 +244,52 @@ export function getObsidianNodeAppearance(node, maxDegree = 1) {
242
244
  };
243
245
  }
244
246
 
247
+ function getObsidianNodeColorSeed(node, group = resolveRelationshipNodeGroup(node)) {
248
+ return mixHash(hashString([
249
+ String(node?.id ?? ""),
250
+ String(node?.name ?? ""),
251
+ group.key,
252
+ String(node?.species ?? ""),
253
+ String(node?.identity ?? "")
254
+ ].join("|")));
255
+ }
256
+
257
+ export function getObsidianNodeAppearance(node, maxDegree = 1) {
258
+ const group = resolveRelationshipNodeGroup(node);
259
+ const paletteIndex = getObsidianNodeColorSeed(node, group) % OBSIDIAN_NODE_PALETTE.length;
260
+ return createObsidianNodeAppearance(node, maxDegree, group, paletteIndex);
261
+ }
262
+
263
+ export function assignBalancedObsidianNodeAppearances(nodes, maxDegree = 1) {
264
+ // 高连接节点优先占用尚未使用的颜色,再按稳定散列打散同一轮的色槽顺序。
265
+ const orderedNodes = [...(Array.isArray(nodes) ? nodes : [])].sort((left, right) => (
266
+ Number(right?.degree ?? 0) - Number(left?.degree ?? 0)
267
+ || Number(right?.weightedDegree ?? 0) - Number(left?.weightedDegree ?? 0)
268
+ || String(left?.name ?? "").localeCompare(String(right?.name ?? ""), "zh-CN")
269
+ || String(left?.id ?? "").localeCompare(String(right?.id ?? ""))
270
+ ));
271
+ const paletteUsage = Array.from({ length: OBSIDIAN_NODE_PALETTE.length }, () => 0);
272
+ const appearances = new Map();
273
+ for (const node of orderedNodes) {
274
+ const group = resolveRelationshipNodeGroup(node);
275
+ const seed = getObsidianNodeColorSeed(node, group);
276
+ const minimumUsage = Math.min(...paletteUsage);
277
+ let paletteIndex = 0;
278
+ let bestScore = -1;
279
+ for (let index = 0; index < paletteUsage.length; index += 1) {
280
+ if (paletteUsage[index] !== minimumUsage) continue;
281
+ const score = mixHash(seed ^ Math.imul(index + 1, 0x9e3779b1));
282
+ if (score > bestScore) {
283
+ bestScore = score;
284
+ paletteIndex = index;
285
+ }
286
+ }
287
+ paletteUsage[paletteIndex] += 1;
288
+ appearances.set(String(node?.id ?? node?.name ?? appearances.size), createObsidianNodeAppearance(node, maxDegree, group, paletteIndex));
289
+ }
290
+ return appearances;
291
+ }
292
+
245
293
  function hashString(value) {
246
294
  let hash = 2166136261;
247
295
  for (let index = 0; index < value.length; index += 1) {
@@ -341,7 +389,10 @@ export function buildRelationshipGraph(characters, relationships) {
341
389
  const maxDegree = Math.max(1, ...nodes.map((node) => node.degree));
342
390
  for (const node of nodes) {
343
391
  node.importance = node.weightedDegree + Math.sqrt(node.degree) * 0.8;
344
- const appearance = getObsidianNodeAppearance(node, maxDegree);
392
+ }
393
+ const nodeAppearances = assignBalancedObsidianNodeAppearances(nodes, maxDegree);
394
+ for (const node of nodes) {
395
+ const appearance = nodeAppearances.get(node.id) ?? getObsidianNodeAppearance(node, maxDegree);
345
396
  node.color = appearance.color;
346
397
  node.darkColor = appearance.darkColor;
347
398
  node.glow = appearance.glow;
@@ -854,7 +905,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
854
905
 
855
906
  const focusBadge = document.createElement("div");
856
907
  focusBadge.className = "relationship-network-focus";
857
- focusBadge.innerHTML = "<strong>人物关系图谱</strong><span>力导向布局 · 按阵营/种族着色</span>";
908
+ focusBadge.innerHTML = "<strong>人物关系图谱</strong><span>力导向布局 · 稳定均衡配色</span>";
858
909
  const focusText = focusBadge.querySelector("span");
859
910
  const help = document.createElement("div");
860
911
  help.className = "relationship-network-help";
@@ -1212,7 +1263,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
1212
1263
  edgeElements.forEach((item) => {
1213
1264
  item.path.classList.remove("is-highlighted", "is-dimmed");
1214
1265
  });
1215
- focusText.textContent = "力导向布局 · 按阵营/种族着色";
1266
+ focusText.textContent = "力导向布局 · 稳定均衡配色";
1216
1267
  edgeDetail.classList.add("hidden");
1217
1268
  edgeDetail.replaceChildren();
1218
1269
  };
@@ -1629,26 +1680,32 @@ export function layoutGalaxy(graph, seed) {
1629
1680
  return { nodes, byId };
1630
1681
  }
1631
1682
 
1632
- export function createGalaxyStarfield(seed, count = 7200) {
1683
+ export function createGalaxyStarfield(seed, count) {
1633
1684
  const random = seededRandom(hashString(seed));
1634
1685
  const stars = [];
1635
1686
  const armCount = 4;
1636
- for (let index = 0; index < count; index += 1) {
1687
+ const baseCount = count === undefined ? GALAXY_BASE_STAR_COUNT : Math.max(0, Math.floor(Number(count) || 0));
1688
+ const edgeBoostCount = count === undefined ? Math.round(baseCount * GALAXY_EDGE_STAR_BOOST_RATIO) : 0;
1689
+ const totalCount = baseCount + edgeBoostCount;
1690
+ for (let index = 0; index < totalCount; index += 1) {
1691
+ const isEdgeBoost = index >= baseCount;
1637
1692
  const population = random();
1638
- const isCore = population < 0.62;
1639
- const isHalo = !isCore && population > 0.9;
1640
- const radius = isCore
1693
+ const isCore = !isEdgeBoost && population < 0.62;
1694
+ const isHalo = !isEdgeBoost && !isCore && population > 0.9;
1695
+ const radius = isEdgeBoost
1696
+ ? 900 + Math.pow(random(), 0.82) * 900
1697
+ : isCore
1641
1698
  ? 32 + Math.pow(random(), 1.72) * 720
1642
1699
  : 160 + Math.pow(random(), 0.68) * 1510;
1643
1700
  const arm = index % armCount;
1644
1701
  const armAngle = arm / armCount * Math.PI * 2;
1645
1702
  const angle = isHalo
1646
1703
  ? random() * Math.PI * 2
1647
- : armAngle + radius * 0.0065 + (random() - 0.5) * (isCore ? 0.72 : 0.42 + radius / 1100);
1648
- const thickness = isHalo ? 70 + radius * 0.2 : (isCore ? 8 + radius * 0.045 : 22 + radius * 0.105);
1704
+ : armAngle + radius * 0.0065 + (random() - 0.5) * (isEdgeBoost ? 0.36 + radius / 1700 : isCore ? 0.72 : 0.42 + radius / 1100);
1705
+ const thickness = isHalo ? 70 + radius * 0.2 : (isEdgeBoost ? 26 + radius * 0.11 : isCore ? 8 + radius * 0.045 : 22 + radius * 0.105);
1649
1706
  const temperature = random();
1650
- const x = Math.cos(angle) * radius + (random() - 0.5) * (isCore ? 42 : 78);
1651
- const z = Math.sin(angle) * radius + (random() - 0.5) * (isCore ? 42 : 78);
1707
+ const x = Math.cos(angle) * radius + (random() - 0.5) * (isCore ? 42 : isEdgeBoost ? 68 : 78);
1708
+ const z = Math.sin(angle) * radius + (random() - 0.5) * (isCore ? 42 : isEdgeBoost ? 68 : 78);
1652
1709
  stars.push({
1653
1710
  x,
1654
1711
  y: (random() + random() + random() - 1.5) * thickness,
@@ -1659,7 +1716,8 @@ export function createGalaxyStarfield(seed, count = 7200) {
1659
1716
  vz: 0,
1660
1717
  size: isCore && random() > 0.94 ? 1.25 + random() * 1.25 : 0.38 + random() * 0.82,
1661
1718
  brightness: isCore ? 0.3 + random() * 0.7 : 0.16 + random() * 0.66,
1662
- color: temperature < 0.2 ? "255,218,176" : temperature > 0.78 ? "174,211,255" : "226,237,255"
1719
+ color: temperature < 0.2 ? "255,218,176" : temperature > 0.78 ? "174,211,255" : "226,237,255",
1720
+ region: isEdgeBoost ? "edge-arm" : isCore ? "core" : isHalo ? "halo" : "arm"
1663
1721
  });
1664
1722
  }
1665
1723
  return stars;
@@ -547,13 +547,48 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
547
547
  .settings-parent-button { white-space: nowrap; }
548
548
  .settings-hub-header { align-items: center; }
549
549
  .settings-hub-header h1 { font-size: clamp(26px, 3vw, 36px); line-height: 1.15; letter-spacing: -.02em; }
550
- .settings-hub-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; max-width: 1000px; margin: 0 auto; }
550
+ .settings-hub-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; width: 100%; max-width: 1000px; margin: 0 auto; }
551
551
  .settings-hub-card { display: grid; grid-template-columns: 48px minmax(0, 1fr); grid-template-rows: auto auto; gap: 4px 14px; align-items: center; min-height: 128px; padding: 22px; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-soft); text-align: left; }
552
552
  .settings-hub-card:hover, .settings-hub-card:focus-visible { border-color: var(--accent); background: var(--paper-deep); box-shadow: var(--shadow); transform: translateY(-2px); }
553
553
  .settings-hub-card:disabled { cursor: not-allowed; opacity: .42; box-shadow: none; transform: none; }
554
554
  .settings-card-mark { display: grid; grid-row: 1 / 3; place-items: center; width: 48px; height: 48px; border: 1px solid var(--line); border-radius: 50%; color: var(--accent-dark); background: var(--surface); font-size: 14px; font-weight: 700; }
555
555
  .settings-hub-card strong { align-self: end; font-size: 16px; }
556
556
  .settings-hub-card small { align-self: start; color: var(--muted); font-size: 10px; line-height: 1.5; }
557
+ .dialog.writing-progress-dialog { width: min(900px, 94vw); }
558
+ .writing-progress-dialog form { display: grid; }
559
+ .writing-progress-body { display: grid; gap: 18px; max-height: calc(88vh - 150px); padding: 20px 24px; overflow-y: auto; }
560
+ .writing-progress-stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
561
+ .writing-progress-stats article { display: grid; align-content: center; gap: 7px; min-height: 78px; padding: 13px 15px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
562
+ .writing-progress-stats small { color: var(--muted); font-size: 9px; }
563
+ .writing-progress-stats strong { font: 20px/1.2 var(--font-latin), monospace; }
564
+ .writing-trend-chart { position: relative; display: grid; grid-template-columns: repeat(30, minmax(4px, 1fr)); align-items: end; gap: 3px; height: 210px; margin-top: 14px; padding: 14px 10px 10px; border: 1px solid var(--line); border-radius: 6px; background: linear-gradient(to top, var(--panel) 0 22px, transparent 22px), linear-gradient(to top, var(--line) 1px, transparent 1px) 0 0 / 100% 25%; }
565
+ .writing-trend-bar { display: grid; grid-template-rows: minmax(0, 1fr) 22px; align-items: end; height: 100%; min-width: 0; cursor: default; }
566
+ .writing-trend-bar:focus-visible { outline: none; }
567
+ .writing-trend-bar i { display: block; justify-self: center; width: clamp(4px, 68%, 14px); height: var(--bar-height); min-height: 3px; border-radius: 4px 4px 1px 1px; background: color-mix(in srgb, var(--accent) 62%, var(--surface)); transition: background-color .14s ease, box-shadow .14s ease, transform .14s ease; }
568
+ .writing-trend-bar.is-today i { background: var(--accent); }
569
+ .writing-trend-bar:hover i, .writing-trend-bar:focus-visible i { background: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); transform: translateY(-2px); }
570
+ .writing-trend-bar small { align-self: center; justify-self: center; overflow: visible; color: var(--muted); font: 7px/22px var(--font-latin), monospace; white-space: nowrap; }
571
+ .writing-trend-daily-line { position: absolute; z-index: 1; top: 14px; right: 10px; bottom: 32px; left: 10px; width: calc(100% - 20px); height: calc(100% - 46px); overflow: visible; pointer-events: none; }
572
+ .writing-trend-daily-line polyline { fill: none; stroke: var(--green); stroke-width: 3; vector-effect: non-scaling-stroke; }
573
+ .writing-trend-daily-points { position: absolute; z-index: 2; top: 14px; right: 10px; bottom: 32px; left: 10px; pointer-events: none; }
574
+ .writing-trend-daily-points i { position: absolute; top: var(--point-y); left: var(--point-x); box-sizing: border-box; width: 7px; height: 7px; border: 2px solid var(--green); border-radius: 50%; background: var(--panel); transform: translate(-50%, -50%); }
575
+ .writing-trend-legend { position: absolute; z-index: 2; top: -27px; right: 0; display: flex; align-items: center; gap: 5px; padding: 3px 0; color: var(--muted); font: 8px/1.4 var(--font-latin), monospace; pointer-events: none; }
576
+ .writing-trend-legend i { width: 10px; height: 5px; border-radius: 2px 2px 0 0; background: color-mix(in srgb, var(--accent) 62%, var(--surface)); }
577
+ .writing-trend-legend b { width: 12px; height: 0; margin-left: 3px; border-top: 2px solid var(--green); }
578
+ .writing-trend-tooltip { position: absolute; z-index: 3; width: 250px; max-width: calc(100% - 16px); padding: 7px 9px; border: 1px solid color-mix(in srgb, var(--ink) 24%, transparent); border-radius: 5px; background: color-mix(in srgb, var(--ink) 94%, transparent); box-shadow: 0 8px 22px rgba(0,0,0,.22); color: var(--panel); font: 10px/1.45 var(--font-latin), monospace; overflow-wrap: anywhere; text-align: center; pointer-events: none; transform: translate(-50%, -100%); }
579
+ .writing-trend-tooltip[hidden] { display: none; }
580
+ .writing-goal-fields { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
581
+ .writing-goal-fields label { display: grid; gap: 7px; color: var(--muted); font-size: 10px; font-weight: 650; }
582
+ .writing-goal-fields input { width: 100%; min-height: 38px; padding: 8px 10px; background: var(--surface); font-size: 12px; font-weight: 500; }
583
+ @media (max-width: 680px) { .writing-progress-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .writing-goal-fields { grid-template-columns: 1fr; } }
584
+ .work-audit-dialog { width: min(860px, 94vw); }
585
+ .work-audit-list { display: grid; max-height: 62vh; overflow: auto; }
586
+ .work-audit-row { display: grid; grid-template-columns: 148px minmax(0, 1fr); gap: 16px; padding: 13px 24px; border-bottom: 1px solid var(--line); }
587
+ .work-audit-row time { color: var(--muted); font: 10px/1.5 var(--font-mono), monospace; }
588
+ .work-audit-row > div { display: grid; min-width: 0; gap: 4px; }
589
+ .work-audit-row strong { font-size: 12px; }
590
+ .work-audit-row span, .work-audit-row small { overflow-wrap: anywhere; color: var(--muted); font-size: 9px; line-height: 1.5; }
591
+ @media (max-width: 620px) { .work-audit-row { grid-template-columns: 1fr; gap: 4px; } }
557
592
  .settings-work-note { max-width: 1000px; margin: 18px auto 0; color: var(--muted); font-size: 11px; }
558
593
  .usage-page-content { width: 100%; max-width: 1400px; margin: 0 auto; padding-top: 0; }
559
594
  .usage-overview { min-width: 0; container-name: usage-overview; container-type: inline-size; }
@@ -733,6 +768,7 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
733
768
  .app-shell.prose-hidden-mode:not(.shelf-mode) #novel-tree { display: none !important; }
734
769
  .app-shell.prose-read-only-mode:not(.shelf-mode) #tidy-blank-lines-button,
735
770
  .app-shell.prose-read-only-mode:not(.shelf-mode) #save-button,
771
+ .app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-delete-button,
736
772
  .app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-edit-button { display: none !important; }
737
773
  .app-shell.prose-read-only-mode:not(.shelf-mode) .left-primary-actions { justify-content: flex-end; }
738
774
  .app-shell.prose-read-only-mode:not(.shelf-mode) #chapter-title[readonly],
@@ -741,6 +777,7 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
741
777
  .app-shell.view-only-mode:not(.shelf-mode) [data-work-settings],
742
778
  .app-shell.view-only-mode:not(.shelf-mode) #tidy-blank-lines-button,
743
779
  .app-shell.view-only-mode:not(.shelf-mode) #save-button,
780
+ .app-shell.view-only-mode:not(.shelf-mode) #chapter-delete-button,
744
781
  .app-shell.view-only-mode:not(.shelf-mode) #chapter-edit-button { display: none !important; }
745
782
  .app-shell.view-only-mode:not(.shelf-mode) .left-primary-actions { justify-content: flex-end; }
746
783
  .app-shell.view-only-mode:not(.shelf-mode) #chapter-title[readonly],
@@ -1054,6 +1091,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1054
1091
  #module-more-button { grid-column: 1 / -1; color: var(--accent-dark); text-align: center; }
1055
1092
  .panel-heading { display: flex; align-items: center; gap: 8px; padding: 15px 0 9px 7px; color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
1056
1093
  .panel-heading-actions { display: flex; align-items: center; gap: 7px; margin-left: auto; }
1094
+ .chapter-batch-button { display: grid; flex: none; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1095
+ .chapter-batch-button svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
1096
+ .chapter-batch-button:hover, .chapter-batch-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1057
1097
  .novel-tree { font-size: 13px; }
1058
1098
  .empty-copy { color: var(--muted); padding: 14px 7px; line-height: 1.5; }
1059
1099
  .volume-node { margin-bottom: 8px; }
@@ -1067,11 +1107,40 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1067
1107
  .volume-node.is-collapsed .volume-chapters { display: none; }
1068
1108
  .chapter-node { display: grid; grid-template-columns: minmax(0, 1fr) max-content; gap: 8px; width: calc(100% + 6px); padding: 9px 8px 9px 20px; border: 0; background: transparent; border-radius: 4px; text-align: left; font-size: 12px; }
1069
1109
  .chapter-node:hover, .chapter-node.active { background: var(--row-active); }
1110
+ .chapter-node[draggable="true"] { cursor: grab; }
1111
+ .chapter-node.is-dragging { opacity: .42; cursor: grabbing; }
1112
+ .chapter-node.is-drag-over { box-shadow: inset 0 2px 0 var(--accent); }
1113
+ .chapter-node.is-drag-over.drop-after { box-shadow: inset 0 -2px 0 var(--accent); }
1114
+ .volume-node.is-drag-target > .volume-title { border-radius: 4px; background: color-mix(in srgb, var(--accent) 12%, var(--surface)); outline: 1px dashed var(--accent); }
1070
1115
  .chapter-node.active { color: var(--accent-dark); font-weight: 700; }
1071
1116
  .chapter-node > span:first-child { min-width: 0; overflow-wrap: anywhere; line-height: 1.45; white-space: normal; }
1072
1117
  .chapter-node-meta { display: flex; flex: none; align-items: center; gap: 5px; margin-left: auto; overflow: visible; white-space: nowrap; }
1073
1118
  .chapter-node small { color: var(--muted); font-size: 9px; }
1074
1119
  .chapter-type-badge { max-width: 58px; overflow: hidden; padding: 2px 5px; border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line)); border-radius: 9px; background: color-mix(in srgb, var(--accent) 12%, var(--surface)); color: var(--accent-dark); font-size: 8px; font-style: normal; text-overflow: ellipsis; white-space: nowrap; }
1120
+ .chapter-batch-dialog { width: min(720px, calc(100vw - 32px)); }
1121
+ .chapter-batch-dialog form { display: grid; gap: 14px; }
1122
+ .chapter-batch-toolbar { display: flex; align-items: center; gap: 8px; margin: 0 24px; }
1123
+ .chapter-batch-toolbar output { margin-left: auto; color: var(--muted); font-size: 11px; }
1124
+ .chapter-batch-search { display: grid; gap: 6px; margin: 0 24px; }
1125
+ .chapter-batch-search > label { color: var(--muted); font-size: 10px; font-weight: 650; }
1126
+ .chapter-batch-search-field { position: relative; display: flex; align-items: center; }
1127
+ .chapter-batch-search-field svg { position: absolute; left: 11px; width: 15px; height: 15px; fill: none; stroke: var(--muted); stroke-width: 1.7; stroke-linecap: round; pointer-events: none; }
1128
+ .chapter-batch-search-field input { width: 100%; min-height: 38px; padding: 8px 68px 8px 34px; background: var(--surface); font-size: 12px; }
1129
+ .chapter-batch-search-field input::-webkit-search-cancel-button { cursor: pointer; }
1130
+ .chapter-batch-search-field output { position: absolute; right: 11px; color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; pointer-events: none; }
1131
+ .chapter-batch-list { display: grid; max-height: min(44vh, 420px); margin: 0 24px; overflow: auto; border: 1px solid var(--line); border-radius: 5px; }
1132
+ .chapter-batch-item { display: grid; grid-template-columns: 18px minmax(0, 1fr); align-items: center; gap: 9px; padding: 9px 11px; border-bottom: 1px solid var(--line); }
1133
+ .chapter-batch-item:last-child { border-bottom: 0; }
1134
+ .chapter-batch-item span { display: grid; min-width: 0; gap: 2px; }
1135
+ .chapter-batch-item strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
1136
+ .chapter-batch-item small { color: var(--muted); font-size: 9px; }
1137
+ .chapter-batch-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin: 0 24px; padding: 14px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
1138
+ .chapter-batch-controls label { display: grid; min-width: 0; gap: 7px; color: var(--muted); font-size: 11px; font-weight: 650; line-height: 1.35; }
1139
+ .chapter-batch-controls select { width: 100%; min-width: 0; min-height: 40px; appearance: none; -webkit-appearance: none; padding: 8px 40px 8px 12px; border-color: color-mix(in srgb, var(--line) 82%, var(--ink)); border-radius: 5px; background-color: var(--surface); background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%); background-position: right 20px center, right 15px center; background-repeat: no-repeat; background-size: 5px 5px, 5px 5px; color: var(--ink); font-size: 12px; font-weight: 500; line-height: 1.4; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease; }
1140
+ .chapter-batch-controls select:hover:not(:disabled) { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); }
1141
+ .chapter-batch-controls select:focus-visible { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); }
1142
+ .chapter-batch-controls select:disabled { cursor: not-allowed; opacity: .52; }
1143
+ @media (max-width: 620px) { .chapter-batch-controls { grid-template-columns: 1fr; } }
1075
1144
 
1076
1145
  .ghost-button, .primary-button { border-radius: 4px; padding: 8px 13px; font-size: 12px; }
1077
1146
  .ghost-button { border: 1px solid var(--line); background: transparent; }
@@ -2107,9 +2176,23 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2107
2176
  .chapter-type-menu > div { display: grid; gap: 3px; }
2108
2177
  .chapter-type-menu button { border: 0; border-radius: 3px; padding: 7px 8px; background: transparent; text-align: left; font-size: 11px; }
2109
2178
  .chapter-type-menu button:hover, .chapter-type-menu button.active { background: var(--paper-deep); color: var(--accent-dark); }
2179
+ .chapter-type-menu .danger-button { margin-top: 5px; border-top: 1px solid var(--line); border-radius: 0; color: var(--accent-dark); }
2110
2180
  .line-citation-menu { position: fixed; z-index: 82; display: grid; min-width: 230px; padding: 8px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: 0 14px 38px rgba(49,42,32,.22); }
2111
2181
  .line-citation-menu strong { padding: 5px 7px 8px; color: var(--muted); font-size: 9px; }
2112
2182
  .line-citation-menu button { border: 0; border-radius: 4px; padding: 8px; background: transparent; text-align: left; font-size: 11px; }.line-citation-menu button:hover { background: var(--paper-deep); color: var(--accent-dark); }
2183
+ .chapter-annotations-dialog { width: min(820px, 94vw); }
2184
+ .chapter-annotations-list { display: grid; gap: 10px; max-height: 62vh; overflow: auto; padding: 18px 24px; }
2185
+ .chapter-annotation-card { display: grid; gap: 8px; padding: 13px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface-soft); }
2186
+ .chapter-annotation-card.is-resolved { opacity: .65; }
2187
+ .chapter-annotation-card header { display: flex; align-items: center; gap: 8px; }
2188
+ .chapter-annotation-card header strong { font-size: 11px; }
2189
+ .chapter-annotation-card header em { margin-left: auto; color: var(--muted); font-size: 9px; font-style: normal; }
2190
+ .chapter-annotation-kind { padding: 2px 6px; border: 1px solid var(--accent); border-radius: 10px; color: var(--accent-dark); font-size: 8px; }
2191
+ .chapter-annotation-card blockquote { max-height: 100px; overflow: auto; margin: 0; padding: 8px 10px; border-left: 3px solid var(--line); background: var(--surface); color: var(--muted); font: 10px/1.6 var(--font-mono), monospace; white-space: pre-wrap; }
2192
+ .chapter-annotation-card p { margin: 0; font-size: 12px; line-height: 1.6; white-space: pre-wrap; }
2193
+ .chapter-annotation-card > small { color: var(--muted); font-size: 9px; }
2194
+ .chapter-annotation-card footer { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; }
2195
+ .chapter-annotation-card footer button { min-height: 30px; padding: 5px 9px; font-size: 9px; }
2113
2196
  @keyframes toast-appear-top { from { opacity: 0; transform: translateY(-7px); } }
2114
2197
  @keyframes toast-appear-bottom { from { opacity: 0; transform: translateY(7px); } }
2115
2198
 
@@ -2625,7 +2708,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2625
2708
  .task-auto-run-progress { grid-column: 1; grid-row: auto; justify-self: stretch; width: 100%; }
2626
2709
  .task-auto-run-progress-ring { display: none; }
2627
2710
  .task-auto-run-progress-bar-layout { display: grid; gap: 6px; width: 100%; }
2628
- .settings-hub-grid { grid-template-columns: 1fr; }
2711
+ .settings-hub-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2629
2712
  .usage-stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2630
2713
  .settings-layout-toolbar, .module-layout-toolbar { justify-content: stretch; }
2631
2714
  .settings-layout-toggle, .module-layout-toggle { width: 100%; }