diff --git a/docs/design/match.md b/docs/design/match.md
index a374946c..e1ef1ebe 100644
--- a/docs/design/match.md
+++ b/docs/design/match.md
@@ -99,7 +99,7 @@ Official rule (bwiki 盟约, 更新公告 5114): before a match the player canno
**Sim:** `simdata` resolves a unit def for `(chessId, skillIndex, moduleId)` (cached): `skill` = the selected SkillRecord; stats = `statsBase` + module attr (elite) ; trait/talents = base with the selected module's overrides. Kit contract (backward compatible): a kit function `(bb, chess) => { skill?, skills?: { [skillId]: SkillSpec }, talents, trait }` receives `chess.skill` = the **selected** skill and `bb` = its blackboard; the loader uses `skills[selectedSkillId]` if present, else `skill` only when the selected skill is the chess's default, else the **generic** skill spec — talents/trait from the kit always apply. Every visible chess must get hand-authored specs for all of its selectable skills (tracked by a coverage test).
-**UI:** a 干员调配 screen reachable from the lobby, room and briefing (INFO_CHECK): filter by tier/bond/class, per chess choose skill (icon, name, SP info, desc at Lv4 and Lv7) and elite module (official type icon from the local-client art when extracted (§13), else a lettered tile; name/type, stat bonus, trait/talent changes), 恢复默认, persisted; a 局内数值 section between the skills and the modules (普通 / 精锐 toggle, 精锐 first) draws the detail card's own stats block, 特性 and 天赋 for the chosen skill / module (§22.16); the chosen skill/module shown on shop cards and in the detail panel (a teammate's unit — 前往查看 in prep, 联防 / 最终攻势 fields, an observed battle — shows **its owner's** skill/module from its UnitInfo `skillIndex` / `moduleId`, `gameLogic.js unitLoadout`) — which also shows the stats, 特性, talents and attack range the unit fights with (`shared/loadoutRecord.js` `loadoutRecord` / `attackRangeGrid`, the same composition the sim's `getChess(id, loadout)` uses; the deploy wheel's range preview too) — the range it is deployed with: a selected passive range skill (引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1) included, over all 1284 loadouts equal to the battle's range at deployment; in battle and prep the card draws the live `range` (§21.16). In battle the renderer plays the equipped skill's own Spine clip and ON_SKILL_START sound (`UnitInfo.skillIndex`; `data/assets.json` lists icons, clips and sounds for every skill index).
+**UI:** a 干员调配 screen reachable from the lobby, room and briefing (INFO_CHECK): the detail starts with the operator's 特质 — the in-match `GarrisonBlock` / RichText of the form its 普通 / 精锐 skill toggle shows, triggers such as 「<进入休整期时>」 kept (0.2.2, PR #301 by @farridge); filter by tier/bond/class, per chess choose skill (icon, name, SP info, desc at Lv4 and Lv7) and elite module (official type icon from the local-client art when extracted (§13), else a lettered tile; name/type, stat bonus, trait/talent changes), 恢复默认, persisted; a 局内数值 section between the skills and the modules (普通 / 精锐 toggle, 精锐 first) draws the detail card's own stats block, 特性 and 天赋 for the chosen skill / module (§22.16); the chosen skill/module shown on shop cards and in the detail panel (a teammate's unit — 前往查看 in prep, 联防 / 最终攻势 fields, an observed battle — shows **its owner's** skill/module from its UnitInfo `skillIndex` / `moduleId`, `gameLogic.js unitLoadout`) — which also shows the stats, 特性, talents and attack range the unit fights with (`shared/loadoutRecord.js` `loadoutRecord` / `attackRangeGrid`, the same composition the sim's `getChess(id, loadout)` uses; the deploy wheel's range preview too) — the range it is deployed with: a selected passive range skill (引星棘刺 S3 3-9) and the 特性's permanent 攻击距离 (信仰搅拌机 SPT-Y +1) included, over all 1284 loadouts equal to the battle's range at deployment; in battle and prep the card draws the live `range` (§21.16). In battle the renderer plays the equipped skill's own Spine clip and ON_SKILL_START sound (`UnitInfo.skillIndex`; `data/assets.json` lists icons, clips and sounds for every skill index).
**导出 / 导入 a loadout:** 导出 hands the current loadout out as a versioned envelope `{ kind: 'stronghold.loadout', v, exportedAt, count, entries }` — copied through `ui/clipboard.js copyText` or downloaded as a file. 导入 accepts that envelope, the stored `{ v, entries }` form and a bare `{ [chessId]: { skill, module } }` map (serialised text too), and refuses another tool's `kind`, junk, an empty result and a **newer** `v` (`parseImport` — a newer payload is never mis-read). Both go through `loadoutSync.applyLoadoutEntries` → `sanitizeEntries`, so a preset from another build never sends the server an entry it would refuse, an import that keeps nothing changes nothing, and the result is reported as N imported / M not (`ui/loadoutModel.js`, `screens/loadout.js`).
**干员持有 (0.2.0 补位):** the overlay's second tab marks operators as not owned (the 55 NORMAL chess; per-browser `sp.pref.ownership` = `{ v, notOwned }`, synced as `room.ownership { notOwned }` by the same sync engine as the loadout, `ui/loadoutSync.js installPrefSync`; 导出 / 导入 as `{ kind: 'stronghold.ownership', v, exportedAt, count, notOwned }`). A not-owned chess fights as its official stand-in with the backup skill / module — the loadout never applies to it ("对于补位干员其技能不可更改") — and is shown as the stand-in wherever the player's copy appears (shop / reward cards, hand and 临时整备区, board, detail card, result lineup, a teammate's scouting and bond popups: portrait, name, model, with a small 「替补」 mark; the owner's recall of the official mode, 2026-10-06), while its bonds, 特质, tier, price and merge stay the chess's; the 0.2.0 补位 design subsections have the rules and sources.
diff --git a/public/css/screens/loadout.css b/public/css/screens/loadout.css
index b497800d..abff46b8 100644
--- a/public/css/screens/loadout.css
+++ b/public/css/screens/loadout.css
@@ -236,6 +236,11 @@
.lo-dhead__reset { position: absolute; right: .12rem; top: .12rem; }
.lo-detail__body { flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; overscroll-behavior: contain; padding: .12rem .18rem .2rem; scrollbar-width: thin; scrollbar-color: var(--line-3) transparent; }
+.lo-garrisons { margin-bottom: .18rem; }
+.lo-garrisons .dgarrison__k { font-size: max(.14rem, 11px); }
+.lo-garrisons .dgarrison__type { font-size: max(.12rem, 10px); }
+.lo-garrisons .dgarrison__icon { width: max(.18rem, 14px); height: max(.18rem, 14px); }
+.lo-garrisons .dgarrison__text { font-size: max(.15rem, 11px); }
.lo-sec { display: flex; flex-direction: column; gap: .08rem; }
.lo-sec + .lo-sec { margin-top: .18rem; }
.lo-sec__head { display: flex; align-items: center; gap: .12rem; min-height: max(.36rem, 22px); }
diff --git a/public/i18n/en.json b/public/i18n/en.json
index bbf29fcf..2e5ee1d3 100644
--- a/public/i18n/en.json
+++ b/public/i18n/en.json
@@ -1055,5 +1055,11 @@
"选择「{name}」的朝向": "Choose {name}'s facing",
"选择朝向": "Choose a facing",
"朝向:{dir}": "Facing: {dir}",
- "开发版 · 不稳定,请勿用于公开服务器": "Development build · unstable, not for public servers"
+ "开发版 · 不稳定,请勿用于公开服务器": "Development build · unstable, not for public servers",
+ "攻回": "Offensive",
+ "受回": "Defensive",
+ "自回": "Auto",
+ "常驻": "Always",
+ "瞬发": "Instant",
+ "部署触发": "On deployment"
}
diff --git a/public/i18n/ja.json b/public/i18n/ja.json
index 9986bbbe..4cececc1 100644
--- a/public/i18n/ja.json
+++ b/public/i18n/ja.json
@@ -1061,5 +1061,11 @@
"{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。": "{files}をダウンロードできなかったため、このページを表示できません。保存済みの設定に影響はありません。ページを更新してください。それでも解決しない場合は、広告ブロッカーとネットワークを確認してください。",
"全员无伤!": "全員無傷!",
"生命值减少 −{n}": "耐久値減少 −{n}",
- "当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。": "この言語の画面表示は機械翻訳のため、不正確な場合があります。GitHubでの修正提案を歓迎します。"
+ "当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。": "この言語の画面表示は機械翻訳のため、不正確な場合があります。GitHubでの修正提案を歓迎します。",
+ "攻回": "攻撃",
+ "受回": "被撃",
+ "自回": "自動",
+ "常驻": "常時",
+ "瞬发": "即時",
+ "部署触发": "配置時"
}
diff --git a/public/i18n/ko.json b/public/i18n/ko.json
index daf09d7f..72d11a73 100644
--- a/public/i18n/ko.json
+++ b/public/i18n/ko.json
@@ -1057,5 +1057,11 @@
"{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。": "{files} 다운로드에 실패하여 이 페이지를 표시할 수 없습니다. 저장된 설정에는 영향이 없습니다. 페이지를 새로고침해 주세요. 그래도 안 되면 광고 차단 확장 프로그램과 네트워크를 확인해 주세요.",
"全员无伤!": "전원 무사!",
"生命值减少 −{n}": "목표 HP 감소 −{n}",
- "当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。": "현재 언어의 인터페이스 텍스트는 기계 번역이므로 정확하지 않을 수 있습니다. GitHub에서 수정 제안을 환영합니다."
+ "当前语言的界面文字为机器翻译,可能不够准确,欢迎在 GitHub 上指正。": "현재 언어의 인터페이스 텍스트는 기계 번역이므로 정확하지 않을 수 있습니다. GitHub에서 수정 제안을 환영합니다.",
+ "攻回": "공격",
+ "受回": "피격",
+ "自回": "자동",
+ "常驻": "상시",
+ "瞬发": "즉시",
+ "部署触发": "배치 시"
}
diff --git a/public/i18n/zh-TW.json b/public/i18n/zh-TW.json
index 24d9f4e9..73effd06 100644
--- a/public/i18n/zh-TW.json
+++ b/public/i18n/zh-TW.json
@@ -1059,5 +1059,11 @@
"游戏数据没有载入": "遊戲資料沒有載入",
"{files} 没有下载成功,这一页无法显示;已保存的设置不受影响。请刷新页面;仍不行时,请检查广告拦截插件和网络。": "{files} 沒有下載成功,這一頁無法顯示;已儲存的設定不受影響。請重新整理頁面;仍不行時,請檢查廣告攔截擴充功能和網路。",
"全员无伤!": "全員無傷!",
- "生命值减少 −{n}": "生命值減少 −{n}"
+ "生命值减少 −{n}": "生命值減少 −{n}",
+ "攻回": "攻回",
+ "受回": "受回",
+ "自回": "自回",
+ "常驻": "常駐",
+ "瞬发": "瞬發",
+ "部署触发": "部署觸發"
}
diff --git a/public/js/screens/loadout.js b/public/js/screens/loadout.js
index ae8e3a14..6f44aaea 100644
--- a/public/js/screens/loadout.js
+++ b/public/js/screens/loadout.js
@@ -22,7 +22,7 @@ import { useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.
import { html, Icon, MicroLabel, Button, TierChip, TextField, Countdown, Spinner, confirmDialog, hasDeadline, Modal, Fragment } from '../ui/components.js';
import { Img, RichText, UnitThumb } from '../ui/gameComponents.js';
import { chessAvatarUrl, chessPortraitUrl, subProfIconUrl, bondIconUrl, moduleTypeIconUrl } from '../ui/assetUrls.js';
-import { chessStatsBlock, traitText, chessTalents } from '../ui/detailPanel.js';
+import { chessStatsBlock, traitText, chessTalents, GarrisonBlock } from '../ui/detailPanel.js';
import { chessLoadout } from '../ui/gameLogic.js';
import { data, useData, localAsset, DATA_FILES } from '../data.js';
import { useStore } from '../store.js';
@@ -172,6 +172,14 @@ function RosterCard({ m, chess, golden, entries, selected, onPick, notOwned = fa
// ---- detail --------------------------------------------------------------------------------------------------------------
+/** The same attribute records and rich-text renderer as the in-match card, including literal trigger markers. */
+export function LoadoutGarrisons({ chess, m }) {
+ const records = (chess?.garrisonIds || []).map((id) => data.lookup('garrisons', id)).filter(Boolean);
+ return records.length ? html`