@musnows/scriverse 0.9.0 → 0.9.1

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.
@@ -3445,15 +3445,18 @@ async function createNewAiConversation(taskType = "chat") {
3445
3445
  function favoriteCharactersFirst(characters) {
3446
3446
  const items = Array.isArray(characters) ? characters : [];
3447
3447
  return [
3448
- ...items.filter((character) => character.isFavorite === true),
3449
- ...items.filter((character) => character.isFavorite !== true)
3448
+ ...items.filter((character) => character.isPinned === true && character.isFavorite === true),
3449
+ ...items.filter((character) => character.isPinned === true && character.isFavorite !== true),
3450
+ ...items.filter((character) => character.isPinned !== true && character.isFavorite === true),
3451
+ ...items.filter((character) => character.isPinned !== true && character.isFavorite !== true)
3450
3452
  ];
3451
3453
  }
3452
3454
 
3453
3455
  function roleplayCharacterOptionLabel(character) {
3454
- const favoriteLabel = character?.isFavorite === true ? "[已收藏] " : "";
3456
+ const pinLabel = character?.isPinned === true ? "[置顶]" : "";
3457
+ const favoriteLabel = character?.isFavorite === true ? "[收藏]" : "";
3455
3458
  const deathLabel = character?.isDead ? "(已死亡)" : "";
3456
- return `${favoriteLabel}${String(character?.name ?? "")}${deathLabel}`;
3459
+ return `${pinLabel}${favoriteLabel}${pinLabel || favoriteLabel ? " " : ""}${String(character?.name ?? "")}${deathLabel}`;
3457
3460
  }
3458
3461
 
3459
3462
  function renderAiRoleplayCharacterSelect() {
@@ -4109,6 +4112,31 @@ function aiPromptTextBeforeCursor() {
4109
4112
  return promptTextFromNode(fragment).replace(/\n$/u, "");
4110
4113
  }
4111
4114
 
4115
+ function aiPromptTextFromRange(range, prompt) {
4116
+ const beforeCursor = range.cloneRange();
4117
+ beforeCursor.selectNodeContents(prompt);
4118
+ beforeCursor.setEnd(range.endContainer, range.endOffset);
4119
+ const fragment = document.createElement("div");
4120
+ fragment.append(beforeCursor.cloneContents());
4121
+ return promptTextFromNode(fragment).replace(/\n$/u, "");
4122
+ }
4123
+
4124
+ function aiPromptTextBoundary(prompt, offset) {
4125
+ const walker = document.createTreeWalker(prompt, NodeFilter.SHOW_TEXT);
4126
+ let remaining = Math.max(0, Number(offset) || 0);
4127
+ let lastTextNode = null;
4128
+ while (walker.nextNode()) {
4129
+ const textNode = walker.currentNode;
4130
+ if (textNode.parentElement?.closest("[data-ai-reference-key]")) continue;
4131
+ lastTextNode = textNode;
4132
+ const length = textNode.textContent?.length ?? 0;
4133
+ if (remaining <= length) return { node: textNode, offset: remaining };
4134
+ remaining -= length;
4135
+ }
4136
+ if (lastTextNode) return { node: lastTextNode, offset: lastTextNode.textContent?.length ?? 0 };
4137
+ return { node: prompt, offset: prompt.childNodes.length };
4138
+ }
4139
+
4112
4140
  function hideAiMentionMenu() {
4113
4141
  aiMentionMatch = null;
4114
4142
  aiMentionRange = null;
@@ -4197,12 +4225,14 @@ function selectAiMention(button) {
4197
4225
  id: button.dataset.aiReferenceId,
4198
4226
  name: button.dataset.aiReferenceName
4199
4227
  };
4200
- const range = aiMentionRange.cloneRange();
4201
- const textNode = range.startContainer;
4202
- const localText = textNode.nodeType === Node.TEXT_NODE ? textNode.textContent?.slice(0, range.startOffset) ?? "" : "";
4203
- const localMention = findAiMention(localText);
4228
+ const cursorText = aiPromptTextFromRange(aiMentionRange, prompt);
4229
+ const localMention = findAiMention(cursorText);
4204
4230
  if (!localMention) return hideAiMentionMenu();
4205
- range.setStart(textNode, localMention.start);
4231
+ const range = document.createRange();
4232
+ const startBoundary = aiPromptTextBoundary(prompt, localMention.start);
4233
+ const endBoundary = aiPromptTextBoundary(prompt, cursorText.length);
4234
+ range.setStart(startBoundary.node, startBoundary.offset);
4235
+ range.setEnd(endBoundary.node, endBoundary.offset);
4206
4236
  range.deleteContents();
4207
4237
  const spacer = document.createTextNode(" ");
4208
4238
  range.insertNode(spacer);
@@ -8687,6 +8717,18 @@ function characterFavoriteIconMarkup() {
8687
8717
  return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2L3 9.6l6.2-.9L12 3Z"></path></svg>';
8688
8718
  }
8689
8719
 
8720
+ function pushPinIconMarkup() {
8721
+ return '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M16 9V4h1V2H7v2h1v5c0 1.66-1.34 3-3 3v2h5.97v6l1.03 1 1.03-1v-6H20v-2c-2.21 0-4-1.79-4-4Z"></path></svg>';
8722
+ }
8723
+
8724
+ function characterPinButton(item) {
8725
+ const isPinned = item.isPinned === true;
8726
+ const canPin = canEditModule("characters");
8727
+ const action = isPinned ? "取消置顶" : "置顶";
8728
+ const title = canPin ? action : "当前账户没有角色模块写入权限";
8729
+ return `<button class="character-pin-button${isPinned ? " is-pinned" : ""}" type="button" data-character-pin="${esc(item.id)}" aria-label="${action}角色“${esc(item.name)}”" aria-pressed="${isPinned}" title="${title}" ${canPin ? "" : "disabled"}>${pushPinIconMarkup()}</button>`;
8730
+ }
8731
+
8690
8732
  function characterFavoriteButton(item) {
8691
8733
  const isFavorite = item.isFavorite === true;
8692
8734
  const canFavorite = canEditModule("characters");
@@ -8713,6 +8755,17 @@ function recordFavoriteButton(type, item, { cardControl = false } = {}) {
8713
8755
  return `<button class="record-favorite-button${cardControl ? " is-card-control" : ""}${isFavorite ? " is-favorite" : ""}" type="button" data-record-favorite="${esc(item.id)}" data-record-favorite-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isFavorite}" title="${title}" ${canFavorite ? "" : "disabled"}>${characterFavoriteIconMarkup()}</button>`;
8714
8756
  }
8715
8757
 
8758
+ function recordPinButton(type, item, { cardControl = false } = {}) {
8759
+ const config = recordFavoriteConfigs[type];
8760
+ if (!config) return "";
8761
+ const isPinned = item.isPinned === true;
8762
+ const canPin = canEditModule(config.module);
8763
+ const action = isPinned ? "取消置顶" : "置顶";
8764
+ const title = canPin ? action : `当前账户没有${config.label}模块写入权限`;
8765
+ const name = String(item[config.nameField] ?? "");
8766
+ return `<button class="record-pin-button${cardControl ? " is-card-control" : ""}${isPinned ? " is-pinned" : ""}" type="button" data-record-pin="${esc(item.id)}" data-record-pin-type="${esc(type)}" aria-label="${action}${config.label}“${esc(name)}”" aria-pressed="${isPinned}" title="${title}" ${canPin ? "" : "disabled"}>${pushPinIconMarkup()}</button>`;
8767
+ }
8768
+
8716
8769
  async function toggleRecordFavorite(type, item, render) {
8717
8770
  const config = recordFavoriteConfigs[type];
8718
8771
  if (!config) return;
@@ -8729,6 +8782,22 @@ async function toggleRecordFavorite(type, item, render) {
8729
8782
  toast(updated.isFavorite ? `已收藏${config.label}“${name}”` : `已取消收藏${config.label}“${name}”`);
8730
8783
  }
8731
8784
 
8785
+ async function toggleRecordPin(type, item, render) {
8786
+ const config = recordFavoriteConfigs[type];
8787
+ if (!config) return;
8788
+ const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/pin`, {
8789
+ method: "PATCH",
8790
+ body: { isPinned: item.isPinned !== true }
8791
+ });
8792
+ await render();
8793
+ const pinButton = [...$("#module-content").querySelectorAll("[data-record-pin]")].find((button) => (
8794
+ button.dataset.recordPinType === type && button.dataset.recordPin === String(updated.id)
8795
+ ));
8796
+ pinButton?.focus({ preventScroll: true });
8797
+ const name = String(updated[config.nameField] ?? "");
8798
+ toast(updated.isPinned ? `已置顶${config.label}“${name}”` : `已取消置顶${config.label}“${name}”`);
8799
+ }
8800
+
8732
8801
  function bindRecordFavoriteButtons(type, items, render) {
8733
8802
  const config = recordFavoriteConfigs[type];
8734
8803
  if (!config) return;
@@ -8745,6 +8814,22 @@ function bindRecordFavoriteButtons(type, items, render) {
8745
8814
  }));
8746
8815
  }
8747
8816
 
8817
+ function bindRecordPinButtons(type, items, render) {
8818
+ const config = recordFavoriteConfigs[type];
8819
+ if (!config) return;
8820
+ $("#module-content").querySelectorAll(`[data-record-pin-type="${type}"]`).forEach((button) => button.addEventListener("click", async () => {
8821
+ const item = items.find((candidate) => candidate.id === button.dataset.recordPin);
8822
+ if (!item || !canEditModule(config.module)) return;
8823
+ button.disabled = true;
8824
+ try {
8825
+ await toggleRecordPin(type, item, render);
8826
+ } catch (error) {
8827
+ button.disabled = false;
8828
+ toast(`${config.label}置顶状态更新失败:${error.message}`, "error");
8829
+ }
8830
+ }));
8831
+ }
8832
+
8748
8833
  function syncEntityDetailFavoriteButton(button, type, item) {
8749
8834
  const config = recordFavoriteConfigs[type];
8750
8835
  const visible = Boolean(config && item);
@@ -8769,6 +8854,30 @@ function syncEntityDetailFavoriteButton(button, type, item) {
8769
8854
  button.disabled = !canFavorite;
8770
8855
  }
8771
8856
 
8857
+ function syncEntityDetailPinButton(button, type, item) {
8858
+ const config = recordFavoriteConfigs[type];
8859
+ const visible = Boolean(config && item);
8860
+ button.classList.toggle("hidden", !visible);
8861
+ button.innerHTML = pushPinIconMarkup();
8862
+ if (!visible) {
8863
+ button.disabled = true;
8864
+ button.classList.remove("is-pinned");
8865
+ button.setAttribute("aria-pressed", "false");
8866
+ button.removeAttribute("aria-label");
8867
+ button.removeAttribute("title");
8868
+ return;
8869
+ }
8870
+ const isPinned = item.isPinned === true;
8871
+ const canPin = canEditModule(config.module);
8872
+ const action = isPinned ? "取消置顶" : "置顶";
8873
+ const name = String(item[config.nameField] ?? "");
8874
+ button.classList.toggle("is-pinned", isPinned);
8875
+ button.setAttribute("aria-pressed", String(isPinned));
8876
+ button.setAttribute("aria-label", `${action}${config.label}“${name}”`);
8877
+ button.title = canPin ? action : `当前账户没有${config.label}模块写入权限`;
8878
+ button.disabled = !canPin;
8879
+ }
8880
+
8772
8881
  function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
8773
8882
  syncEntityDetailFavoriteButton(button, type, getItem());
8774
8883
  button.onclick = async () => {
@@ -8793,6 +8902,30 @@ function bindEntityDetailFavoriteButton(button, type, getItem, onUpdated) {
8793
8902
  };
8794
8903
  }
8795
8904
 
8905
+ function bindEntityDetailPinButton(button, type, getItem, onUpdated) {
8906
+ syncEntityDetailPinButton(button, type, getItem());
8907
+ button.onclick = async () => {
8908
+ const config = recordFavoriteConfigs[type];
8909
+ const item = getItem();
8910
+ if (!config || !item || !canEditModule(config.module)) return;
8911
+ button.disabled = true;
8912
+ try {
8913
+ const updated = await api(`/api/${config.resource}/${encodeURIComponent(item.id)}/pin`, {
8914
+ method: "PATCH",
8915
+ body: { isPinned: item.isPinned !== true }
8916
+ });
8917
+ onUpdated(updated);
8918
+ syncEntityDetailPinButton(button, type, getItem() ?? updated);
8919
+ button.focus({ preventScroll: true });
8920
+ const name = String(updated[config.nameField] ?? "");
8921
+ toast(updated.isPinned ? `已置顶${config.label}“${name}”` : `已取消置顶${config.label}“${name}”`);
8922
+ } catch (error) {
8923
+ syncEntityDetailPinButton(button, type, getItem());
8924
+ toast(`${config.label}置顶状态更新失败:${error instanceof Error ? error.message : "未知错误"}`, "error");
8925
+ }
8926
+ };
8927
+ }
8928
+
8796
8929
  function recordCardEditButton(attribute, id, label) {
8797
8930
  return `<button class="record-card-edit" type="button" data-${attribute}="${esc(id)}" aria-label="编辑${esc(label)}" title="编辑">${pencilIconMarkup()}</button>`;
8798
8931
  }
@@ -9131,7 +9264,7 @@ function settingRecordActions(item) {
9131
9264
  const recordAction = canEditModule("settings")
9132
9265
  ? recordCardEditButton("edit-setting", item.id, `设定“${item.title}”`)
9133
9266
  : recordHistoryButton("setting", item.id, item.title);
9134
- return `${recordFavoriteButton("setting", item)}${recordAction}`;
9267
+ return `${recordPinButton("setting", item)}${recordFavoriteButton("setting", item)}${recordAction}`;
9135
9268
  }
9136
9269
 
9137
9270
  function draftTypeLabel(draftType) {
@@ -9245,7 +9378,11 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
9245
9378
  }
9246
9379
  if (item && viewOnly) {
9247
9380
  const headerActions = $("#dialog-context-actions");
9248
- headerActions.innerHTML = `<button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
9381
+ headerActions.innerHTML = `<button id="draft-dialog-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button><button id="draft-dialog-favorite" class="entity-detail-favorite-button" type="button" aria-pressed="false"></button>${canEditModule("drafts") ? '<button id="draft-dialog-edit" class="ghost-button" type="button">编辑想法</button>' : ""}`;
9382
+ bindEntityDetailPinButton($("#draft-dialog-pin"), "draft", () => draftDialogItem, (updated) => {
9383
+ draftDialogItem = updated;
9384
+ void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
9385
+ });
9249
9386
  bindEntityDetailFavoriteButton($("#draft-dialog-favorite"), "draft", () => draftDialogItem, (updated) => {
9250
9387
  draftDialogItem = updated;
9251
9388
  void renderDrafts(moduleListPages.drafts).catch((error) => toast(`想法列表刷新失败:${error instanceof Error ? error.message : "未知错误"}`, "error"));
@@ -9288,7 +9425,7 @@ async function renderDrafts(page = moduleListPages.drafts) {
9288
9425
  <details class="character-filter-dropdown"><summary><span>按绑定位置筛选</span><strong>${selectedBindingKeys.size ? `已选 ${selectedBindingKeys.size} 项` : "全部位置"}</strong></summary><div id="draft-binding-filter" class="character-filter-options">${bindingOptions.map(([value, label]) => `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedBindingKeys.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`).join("")}</div></details>
9289
9426
  <div class="character-filter-toolbar-actions">${hasDraftFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${drafts.length} 条想法</span>` : ""}<button id="clear-draft-filters" class="ghost-button" type="button" ${hasDraftFilters ? "" : "disabled"}>重置筛选</button></div>
9290
9427
  </section>`;
9291
- const actions = (item) => `${recordFavoriteButton("draft", item)}${canEditModule("drafts")
9428
+ const actions = (item) => `${recordPinButton("draft", item)}${recordFavoriteButton("draft", item)}${canEditModule("drafts")
9292
9429
  ? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
9293
9430
  : recordHistoryButton("draft", item.id, item.title)}`;
9294
9431
  const cards = `<div class="card-grid">${pageResult.items.map((item) => `
@@ -9350,6 +9487,10 @@ async function renderDrafts(page = moduleListPages.drafts) {
9350
9487
  moduleListPages.drafts = 1;
9351
9488
  await renderDrafts(1);
9352
9489
  });
9490
+ bindRecordPinButtons("draft", pageResult.items, async () => {
9491
+ moduleListPages.drafts = 1;
9492
+ await renderDrafts(1);
9493
+ });
9353
9494
  bindEntityHistoryButtons(() => renderDrafts(pageResult.page));
9354
9495
  }
9355
9496
 
@@ -9417,6 +9558,10 @@ async function renderSettings(page = moduleListPages.settings) {
9417
9558
  moduleListPages.settings = 1;
9418
9559
  await renderSettings(1);
9419
9560
  });
9561
+ bindRecordPinButtons("setting", pageResult.items, async () => {
9562
+ moduleListPages.settings = 1;
9563
+ await renderSettings(1);
9564
+ });
9420
9565
  bindEntityHistoryButtons(async () => { await renderSettings(pageResult.page); await loadAiReferences(); });
9421
9566
  };
9422
9567
 
@@ -9468,6 +9613,21 @@ async function toggleCharacterFavorite(item) {
9468
9613
  toast(updated.isFavorite ? `已收藏角色“${updated.name}”` : `已取消收藏角色“${updated.name}”`);
9469
9614
  }
9470
9615
 
9616
+ async function toggleCharacterPin(item) {
9617
+ const updated = await api(`/api/characters/${encodeURIComponent(item.id)}/pin`, {
9618
+ method: "PATCH",
9619
+ body: { isPinned: item.isPinned !== true }
9620
+ });
9621
+ if (loadedAiReferencesWorkId === state.work?.id) {
9622
+ state.characters = upsertEntityCollection(state.characters, updated);
9623
+ renderAiRoleplayCharacterSelect();
9624
+ }
9625
+ characterListPage = 1;
9626
+ await renderCharacters(1);
9627
+ $("#module-content").querySelector(`[data-character-pin="${CSS.escape(String(updated.id))}"]`)?.focus({ preventScroll: true });
9628
+ toast(updated.isPinned ? `已置顶角色“${updated.name}”` : `已取消置顶角色“${updated.name}”`);
9629
+ }
9630
+
9471
9631
  async function renderCharacters(page = characterListPage) {
9472
9632
  const hasCharacterFilters = characterFilters.raceIds.length > 0
9473
9633
  || characterFilters.organizationIds.length > 0
@@ -9490,14 +9650,14 @@ async function renderCharacters(page = characterListPage) {
9490
9650
  [state.races, state.organizations] = [races, organizations];
9491
9651
  mountModuleCount(characterPage.total);
9492
9652
  const layout = readModuleLayout();
9493
- const characterActions = (item) => `${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
9653
+ const characterActions = (item) => `${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}`;
9494
9654
  const characterLockBadge = (item) => item.lockedFields.length
9495
9655
  ? `<span class="character-lock-badge" aria-label="${item.lockedFields.length} 个锁定字段" title="锁定字段:${esc(item.lockedFields.join("、"))}"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="5" y="10" width="14" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg><span>${item.lockedFields.length}</span></span>`
9496
9656
  : "";
9497
9657
  const characterCards = () => `<div class="card-grid">${pageCharacters.map((item) => {
9498
9658
  const details = normalizeCharacterDetails(item.attributes?.details);
9499
9659
  return `
9500
- <article class="record-card character-card preview-record-card has-card-edit" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
9660
+ <article class="record-card character-card preview-record-card has-card-edit has-pin-control" data-open-character="${esc(item.id)}" role="button" tabindex="0" aria-label="查看角色 ${esc(item.name)}">${characterPinButton(item)}${characterFavoriteButton(item)}${recordCardEditButton("edit-character", item.id, `角色“${item.name}”`)}
9501
9661
  <div class="character-card-heading">${characterAvatarHtml(item)}<h3>${esc(item.name)}</h3>${entityLifecycleBadge(item.isDead, "已死亡")}${characterLockBadge(item)}</div>
9502
9662
  ${item.attributes?.identity ? `<p class="character-identity">${esc(item.attributes.identity)}</p>` : ""}
9503
9663
  <div class="character-gender"><b>性别</b><span class="pill">${esc(characterGenderLabel(item.gender))}</span></div>
@@ -9574,6 +9734,17 @@ async function renderCharacters(page = characterListPage) {
9574
9734
  toast(`角色收藏状态更新失败:${error.message}`, "error");
9575
9735
  }
9576
9736
  }));
9737
+ $("#module-content").querySelectorAll("[data-character-pin]").forEach((button) => button.addEventListener("click", async () => {
9738
+ const item = pageCharacters.find((candidate) => candidate.id === button.dataset.characterPin);
9739
+ if (!item || !canEditModule("characters")) return;
9740
+ button.disabled = true;
9741
+ try {
9742
+ await toggleCharacterPin(item);
9743
+ } catch (error) {
9744
+ button.disabled = false;
9745
+ toast(`角色置顶状态更新失败:${error.message}`, "error");
9746
+ }
9747
+ }));
9577
9748
  const readSelectedValues = (selector) => [...$(selector).querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
9578
9749
  $("#character-race-filter").addEventListener("change", async () => {
9579
9750
  characterFiltersPanelOpen = true;
@@ -9728,14 +9899,14 @@ async function renderOrganizations(page = moduleListPages.organizations) {
9728
9899
  moduleListPages.organizations = pageResult.page;
9729
9900
  const layout = readModuleLayout();
9730
9901
  const canEditOrganizations = canEditModule("organizations");
9731
- const organizationActions = (item, { cardControl = false } = {}) => `${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
9902
+ const organizationActions = (item, { cardControl = false } = {}) => `${recordPinButton("organization", item, { cardControl })}${recordFavoriteButton("organization", item, { cardControl })}${canEditOrganizations
9732
9903
  ? recordCardEditButton("edit-organization", item.id, `组织“${item.name}”`)
9733
9904
  : recordHistoryButton("organization", item.id, item.name)}`;
9734
9905
  const organizationCardActions = (item) => canEditOrganizations
9735
9906
  ? organizationActions(item, { cardControl: true })
9736
9907
  : `<div class="card-actions">${organizationActions(item)}</div>`;
9737
9908
  const organizationCards = () => `<div class="card-grid organization-grid">${pageResult.items.map((item) => `
9738
- <article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
9909
+ <article class="record-card organization-card preview-record-card${canEditOrganizations ? " has-card-edit has-favorite-control has-pin-control" : ""}" data-open-organization="${esc(item.id)}" role="button" tabindex="0" aria-label="查看组织 ${esc(item.name)}"><small>${item.memberIds.length} 位成员 · ${(item.settingsCount ?? item.settings?.length ?? 0) ? "已填写组织设定" : "暂无组织设定"}</small>
9739
9910
  <h3>${esc(item.name)}${entityLifecycleBadge(item.isDissolved, "已解散")}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
9740
9911
  <div class="organization-settings">${item.settingsCount ? `<span class="pill">${item.settingsCount} 条组织设定,打开查看详情</span>` : '<span class="pill">暂无组织设定</span>'}</div>
9741
9912
  <p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
@@ -9764,6 +9935,10 @@ async function renderOrganizations(page = moduleListPages.organizations) {
9764
9935
  moduleListPages.organizations = 1;
9765
9936
  await renderOrganizations(1);
9766
9937
  });
9938
+ bindRecordPinButtons("organization", pageResult.items, async () => {
9939
+ moduleListPages.organizations = 1;
9940
+ await renderOrganizations(1);
9941
+ });
9767
9942
  bindEntityHistoryButtons(async () => { await renderOrganizations(pageResult.page); await loadAiReferences(); });
9768
9943
  }
9769
9944
 
@@ -13476,6 +13651,10 @@ function openSettingEditor(item = null, { readOnly = false } = {}) {
13476
13651
  const editButton = $("#setting-editor-edit");
13477
13652
  editButton.classList.toggle("hidden", !readOnly || !canEditModule("settings"));
13478
13653
  editButton.onclick = () => openSettingEditor(settingEditorItem);
13654
+ bindEntityDetailPinButton($("#setting-editor-pin"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
13655
+ settingEditorItem = updated;
13656
+ state.settings = upsertEntityCollection(state.settings, updated);
13657
+ });
13479
13658
  bindEntityDetailFavoriteButton($("#setting-editor-favorite"), "setting", () => viewOnly ? settingEditorItem : null, (updated) => {
13480
13659
  settingEditorItem = updated;
13481
13660
  state.settings = upsertEntityCollection(state.settings, updated);
@@ -14712,6 +14891,13 @@ async function openCharacterEditor(item = null, { readOnly = false } = {}) {
14712
14891
  const editButton = $("#character-editor-edit");
14713
14892
  editButton.classList.toggle("hidden", !readOnly || !canEditModule("characters"));
14714
14893
  editButton.onclick = () => void openCharacterEditor(characterEditorItem);
14894
+ bindEntityDetailPinButton($("#character-editor-pin"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
14895
+ characterEditorItem = updated;
14896
+ if (loadedAiReferencesWorkId === state.work?.id) {
14897
+ state.characters = upsertEntityCollection(state.characters, updated);
14898
+ renderAiRoleplayCharacterSelect();
14899
+ }
14900
+ });
14715
14901
  bindEntityDetailFavoriteButton($("#character-editor-favorite"), "character", () => viewOnly ? characterEditorItem : null, (updated) => {
14716
14902
  characterEditorItem = updated;
14717
14903
  if (loadedAiReferencesWorkId === state.work?.id) {
@@ -14905,6 +15091,10 @@ async function openKnowledgeEditor(kind, item, { readOnly = false } = {}) {
14905
15091
  editButton.textContent = `编辑${label}`;
14906
15092
  editButton.classList.toggle("hidden", !readOnly || !canEditModule(module));
14907
15093
  editButton.onclick = () => void openKnowledgeEditor(kind, knowledgeEditorItem);
15094
+ bindEntityDetailPinButton($("#knowledge-editor-pin"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
15095
+ knowledgeEditorItem = updated;
15096
+ state.organizations = upsertEntityCollection(state.organizations, updated);
15097
+ });
14908
15098
  bindEntityDetailFavoriteButton($("#knowledge-editor-favorite"), "organization", () => !isRace && viewOnly ? knowledgeEditorItem : null, (updated) => {
14909
15099
  knowledgeEditorItem = updated;
14910
15100
  state.organizations = upsertEntityCollection(state.organizations, updated);
@@ -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=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1">
13
+ <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v3&feature=character-card-title-fit-v2">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -658,6 +658,7 @@
658
658
  </div>
659
659
  </div>
660
660
  <div class="entity-editor-mode-actions">
661
+ <button id="setting-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
661
662
  <button id="setting-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
662
663
  <button id="setting-editor-edit" class="ghost-button hidden" type="button">编辑设定</button>
663
664
  <button id="setting-editor-confirm" class="ghost-button hidden" type="button">确认候选</button>
@@ -685,6 +686,7 @@
685
686
  <div class="character-editor-title-row"><h1 id="character-editor-title">新建角色</h1><span id="character-editor-version" class="character-version-badge">新档案</span><span id="character-editor-readonly-badge" class="entity-editor-readonly-badge hidden">只读模式</span></div>
686
687
  </div>
687
688
  <div class="character-editor-header-actions">
689
+ <button id="character-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
688
690
  <button id="character-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
689
691
  <button id="character-editor-edit" class="primary-button hidden" type="button">编辑角色</button>
690
692
  <button id="character-history-button" class="ghost-button" type="button" aria-controls="character-history-panel" aria-expanded="false">版本历史</button>
@@ -721,6 +723,7 @@
721
723
  </div>
722
724
  <div class="character-editor-header-actions">
723
725
  <span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
726
+ <button id="knowledge-editor-pin" class="entity-detail-pin-button hidden" type="button" aria-pressed="false"></button>
724
727
  <button id="knowledge-editor-favorite" class="entity-detail-favorite-button hidden" type="button" aria-pressed="false"></button>
725
728
  <button id="knowledge-editor-edit" class="primary-button hidden" type="button">编辑档案</button>
726
729
  <button id="knowledge-editor-history" class="ghost-button hidden" type="button">版本历史</button>
@@ -1258,6 +1261,6 @@
1258
1261
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1259
1262
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1260
1263
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1261
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1"></script>
1264
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1"></script>
1262
1265
  </body>
1263
1266
  </html>
@@ -1517,9 +1517,16 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1517
1517
  .record-card { position: relative; border: 1px solid var(--line); background: var(--surface-soft); padding: 18px; min-height: 130px; border-radius: 4px; }
1518
1518
  .record-card.has-card-edit h3 { padding-right: 34px; }
1519
1519
  .record-card.has-favorite-control h3 { padding-right: 70px; }
1520
+ .record-card.has-pin-control h3 { padding-right: 106px; }
1520
1521
  .record-card-edit { position: absolute; top: 12px; right: 12px; display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1521
1522
  .record-card-edit:hover, .record-card-edit:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1522
1523
  .record-card-edit svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.7; }
1524
+ .character-pin-button { position: absolute; top: 12px; right: 84px; display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1525
+ .character-pin-button:hover, .character-pin-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1526
+ .character-pin-button.is-pinned { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
1527
+ .character-pin-button:disabled { cursor: not-allowed; opacity: .5; }
1528
+ .character-pin-button svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1529
+ .character-pin-button.is-pinned svg { fill: currentColor; }
1523
1530
  .character-favorite-button { position: absolute; top: 12px; right: 48px; display: inline-grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1524
1531
  .character-favorite-button:hover, .character-favorite-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1525
1532
  .character-favorite-button.is-favorite { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
@@ -1533,12 +1540,25 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1533
1540
  .record-favorite-button:disabled { cursor: not-allowed; opacity: .5; }
1534
1541
  .record-favorite-button svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1535
1542
  .record-favorite-button.is-favorite svg { fill: currentColor; }
1543
+ .record-pin-button { display: inline-grid; flex: 0 0 auto; place-items: center; width: 28px; height: 28px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1544
+ .record-pin-button.is-card-control { position: absolute; top: 12px; right: 84px; }
1545
+ .record-pin-button:hover, .record-pin-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1546
+ .record-pin-button.is-pinned { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
1547
+ .record-pin-button:disabled { cursor: not-allowed; opacity: .5; }
1548
+ .record-pin-button svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1549
+ .record-pin-button.is-pinned svg { fill: currentColor; }
1536
1550
  .entity-detail-favorite-button { display: inline-grid; flex: 0 0 36px; place-items: center; width: 36px; min-width: 36px; height: 36px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1537
1551
  .entity-detail-favorite-button:hover, .entity-detail-favorite-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1538
1552
  .entity-detail-favorite-button.is-favorite { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
1539
1553
  .entity-detail-favorite-button:disabled { cursor: not-allowed; opacity: .5; }
1540
1554
  .entity-detail-favorite-button svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1541
1555
  .entity-detail-favorite-button.is-favorite svg { fill: currentColor; }
1556
+ .entity-detail-pin-button { display: inline-grid; flex: 0 0 36px; place-items: center; width: 36px; min-width: 36px; height: 36px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
1557
+ .entity-detail-pin-button:hover, .entity-detail-pin-button:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: none; }
1558
+ .entity-detail-pin-button.is-pinned { border-color: color-mix(in srgb, var(--accent) 58%, var(--line)); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); color: var(--accent-dark); }
1559
+ .entity-detail-pin-button:disabled { cursor: not-allowed; opacity: .5; }
1560
+ .entity-detail-pin-button svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.6; }
1561
+ .entity-detail-pin-button.is-pinned svg { fill: currentColor; }
1542
1562
  .record-card h3 { font-size: 16px; margin: 5px 0 10px; font-weight: 600; }
1543
1563
  .record-card p { color: var(--muted); font-size: 12px; line-height: 1.55; margin: 0; white-space: pre-wrap; }
1544
1564
  .record-card small { color: var(--accent); font-size: 9px; letter-spacing: .1em; text-transform: uppercase; }
@@ -1569,15 +1589,28 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
1569
1589
  .module-row-span { grid-column: 1 / -1; }
1570
1590
  .setting-row .card-actions, .module-row .card-actions { margin-top: 0; flex-wrap: wrap; justify-content: flex-end; }
1571
1591
  .module-row .card-actions > .record-card-edit { position: static; }
1572
- .character-card { cursor: pointer; }
1592
+ .character-card { cursor: pointer; container: character-card / inline-size; }
1573
1593
  .character-row { grid-template-columns: minmax(140px, .28fr) minmax(0, 1fr) auto; }
1574
1594
  .character-card-heading { display: flex; min-width: 0; align-items: center; gap: 7px; }
1575
1595
  .character-card-heading .character-avatar { width: 32px; height: 32px; font-size: 12px; }
1576
- .character-card-heading h3 { min-width: 0; }
1596
+ .character-card-heading h3 { flex: 1 1 0; min-width: 0; overflow-wrap: anywhere; }
1577
1597
  .record-card.has-card-edit .character-card-heading { padding-right: 70px; }
1598
+ .record-card.has-card-edit.has-pin-control .character-card-heading { padding-right: 106px; }
1578
1599
  .record-card.has-card-edit .character-card-heading h3 { padding-right: 0; }
1600
+ .character-card.has-card-edit > .character-favorite-button, .character-card.has-card-edit > .record-card-edit { top: 21px; }
1579
1601
  .module-row .character-favorite-button { position: static; }
1602
+ .module-row .character-pin-button { position: static; }
1580
1603
  .module-row .record-favorite-button { position: static; }
1604
+ .module-row .record-pin-button { position: static; }
1605
+ @container character-card (max-width: 320px) {
1606
+ .record-card.has-card-edit.has-pin-control .character-card-heading { padding-top: 42px; padding-right: 0; }
1607
+ }
1608
+ @media (max-width: 560px) {
1609
+ .character-card-heading h3 { font-size: 15px; }
1610
+ }
1611
+ @media (max-width: 420px) {
1612
+ .character-card-heading h3 { font-size: 14px; }
1613
+ }
1581
1614
  .character-lock-badge { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 3px; min-height: 20px; padding: 2px 6px; border: 1px solid color-mix(in srgb, var(--accent) 42%, var(--line)); border-radius: 10px; background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent-dark); font-size: 9px; font-variant-numeric: tabular-nums; line-height: 1; }
1582
1615
  .entity-lifecycle-badge { display: inline-flex; align-items: center; min-height: 20px; margin-left: 7px; padding: 2px 7px; border: 1px solid color-mix(in srgb, var(--accent) 58%, var(--line)); border-radius: 10px; background: color-mix(in srgb, var(--accent) 13%, var(--surface)); color: var(--accent-dark); font-size: 9px; font-weight: 650; line-height: 1; vertical-align: middle; white-space: nowrap; }
1583
1616
  .character-lock-badge svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
@@ -4066,6 +4099,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4066
4099
  .module-content { padding: 18px 0 56px; }
4067
4100
  .card-grid, .provider-card-grid { grid-template-columns: minmax(0, 1fr); }
4068
4101
  .record-card { padding: 15px; }
4102
+ .character-card.has-card-edit > .character-favorite-button, .character-card.has-card-edit > .record-card-edit { top: 12px; }
4069
4103
  .card-actions, .task-row-actions, .module-header-actions { gap: 8px; }
4070
4104
  .card-actions button, .task-row-actions button { flex: 1 1 auto; min-height: 38px; }
4071
4105
  .setting-row, .module-row { padding: 13px; }
@@ -4152,8 +4186,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
4152
4186
  .entity-editor-header, .character-editor-header { grid-template-columns: auto minmax(0, 1fr); gap: 8px; padding: 12px var(--mobile-gutter); }
4153
4187
  .entity-editor-mode-actions, .character-editor-header-actions { grid-column: 1 / -1; justify-content: stretch; flex-wrap: wrap; }
4154
4188
  .entity-editor-mode-actions > button, .character-editor-header-actions > button { flex: 1 1 auto; }
4155
- .entity-editor-mode-actions > .entity-detail-favorite-button, .character-editor-header-actions > .entity-detail-favorite-button { flex: 0 0 40px; width: 40px; min-width: 40px; height: 40px; }
4156
- .dialog-context-actions .entity-detail-favorite-button { flex-basis: 40px; width: 40px; min-width: 40px; height: 40px; }
4189
+ .entity-editor-mode-actions > .entity-detail-favorite-button, .entity-editor-mode-actions > .entity-detail-pin-button, .character-editor-header-actions > .entity-detail-favorite-button, .character-editor-header-actions > .entity-detail-pin-button { flex: 0 0 40px; width: 40px; min-width: 40px; height: 40px; }
4190
+ .dialog-context-actions .entity-detail-favorite-button, .dialog-context-actions .entity-detail-pin-button { flex-basis: 40px; width: 40px; min-width: 40px; height: 40px; }
4157
4191
  .setting-editor-header { min-height: 0; }
4158
4192
  .setting-editor-content { padding: 12px; }
4159
4193
  .setting-markdown-field { height: 68dvh; }