# info 元件交付

結論：`confirmed` 已把實驗三 C3 中通過使用者驗收的術語解釋 affordance 收割成可直接組裝的 Info domain；2026-08-10 另收進 judgment 多行變體與 Read × Info fit panel 的條件式鍵盤捲動，2026-08-11 再收進明確 opt-in 的 sheet／dialog modal carriers。Inline `.info-button` 仍只切換自己的 `.info-panel` 並允許多開；modal 由 `InfoModalHost` 管理 shared surface、singleton、focus與inert，不承擔解說文案或presentation mapping。

## 1. 出處與範圍

- `confirmed` 唯一權威來源是實驗三 C3：`design-system-trial/outputs/experiment-03/arms/C3/index.html`。
- `confirmed` 收割日期為 2026-08-08；使用者已驗收觸發按鈕的位置與展開節奏。
- `confirmed` CSS 保留來源的 `.info-stack`、`.info-trigger-row`、`.info-button`、`.info-panel` 視覺規則；reduced motion 另修正為停止 transition。2026-08-10 只追加 judgment 多行排列與受限高度捲動。
- `confirmed` JS 保留來源 `bindInfoControls()` 的行為：按鈕切換 `aria-expanded`，並以 `hidden` 控制對應 panel。
- 2026-08-10 judgment 多行結構來源為 `design-system-trial/outputs/content-design/prototype-04/index.html`；只收 `.judgment-info-panel .info-line` 的多行形狀，不收產品文案或按鈕差異。
- **解說文案的內容規範（30 字內、白話、哪些術語該掛 ℹ）屬消費端 BRIEF／COPY-STYLE 慣例，不屬本契約。** 本契約只管結構與行為。

## 2. 元件拆解

| Before | After | Why |
| --- | --- | --- |
| C3 把術語控制的 CSS 與產品頁樣式寫在同一個 `<style>` | `info.css` 獨立擁有 Info domain 規則 | 讓不同 prototype 直接引用，不再各自重畫 |
| C3 頁面內的 `bindInfoControls()` 綁定全部 `[data-info]` | `info.js` 自動初始化每個 `InfoControl` | 保留原行為，並避免重複綁定 |
| 術語文案與 disclosure 結構混在消費端 | 契約只固定結構、狀態與 a11y | 文案仍由 BRIEF／COPY-STYLE 決定 |

## 3. 狀態與行為

| 狀態／邊界 | 元件行為 | 證據等級 |
| --- | --- | --- |
| collapsed | button 為 `aria-expanded="false"`；對應 panel 有 `hidden` | `confirmed` C3 來源行為 |
| expanded | button 為 `aria-expanded="true"`；對應 panel 移除 `hidden` | `confirmed` C3 來源行為 |
| 多個控制 | 每個 button 只切換自己的 panel；其他已展開 panel 不會自動收合 | `confirmed` C3 允許多個同時展開 |
| 再次點擊 | 在 collapsed／expanded 之間反轉 | `confirmed` C3 來源行為 |
| reduced motion | `.info-button` 停止 transition | `inferred` 修正來源未真正關閉動畫的 bug |
| modal open | Consumer明確指定sheet／dialog；source保持hidden，內容進shared surface | 2026-08-11 modal carrier收割 |
| modal singleton | 同一host至多一顆trigger expanded；開第二顆不回焦第一顆 | 2026-08-11 modal carrier收割 |
| modal close | ESC／scrim／sheet close／dialog action關閉，解除inert並回焦 | 2026-08-11 modal carrier收割 |

Inline元件不加動畫，也不實作單開互斥。Modal sheet使用`--duration-base`進場、layer使用`--duration-fast`，reduced motion時兩者都停止；modal singleton不改變inline多開。button 的 Enter／Space 行為由瀏覽器原生提供；fit panel 聚焦時只攔住捲動鍵往父層 Read viewport 冒泡，保留 panel 自己的原生捲動。

## 4. 可及性契約

- 所有 trigger 必須有 `type="button"`、`data-info`、`aria-expanded` 與 `aria-controls`。
- Inline trigger的`aria-controls`必須指向同一組panel的`id`；button的`data-info`值必須等於panel的`data-info-panel`值。
- Modal trigger的`aria-controls`指向實際shared surface；source改由`data-info`對`data-info-panel`唯一配對，另須`aria-haspopup="dialog"`。目標screen reader尚未驗證，coverage見第10節。
- JS 每次切換都同步寫入 `aria-expanded`；inline以panel的`hidden` property顯示，modal source始終hidden。
- 初始 DOM 必須一致：inline `aria-expanded="false"` 對應 `hidden`；modal trigger初始false且source hidden。
- `.info-button:focus-visible`與modal chrome提供token化focus outline；不得移除native button的鍵盤路徑。

## 5. 組裝契約

Inline必要 DOM：

```html
<div class="info-stack">
  <div class="info-trigger-row">
    <button
      class="info-button"
      type="button"
      data-info="lots"
      aria-expanded="false"
      aria-controls="info-lots"
    >張 ℹ</button>
  </div>
  <p class="info-panel" id="info-lots" data-info-panel="lots" hidden>
    1 張＝1,000 股。
  </p>
</div>
```

結構鏈不可省略：`.info-stack > .info-trigger-row > button.info-button[data-info][aria-expanded][aria-controls]`，panel 則為同一 `.info-stack` 內的 `.info-panel[data-info-panel][id]`。collapsed 狀態才帶 `[hidden]`，expanded 狀態必須移除。一個 `.info-trigger-row` 可放多個 button，一個 `.info-stack` 可放對應的多個 panel。

載入 `info.js` 會自動初始化所有 `[data-info]`。Modal trigger也由`InfoControl`初始化，但`InfoModalHost`必須由composition以constructor options明確mount；必要DOM與API見第10節。每個inline control都支援 `data-info-fit-trigger` 的 cold-start 動態加入／移除；modal trigger不得使用fit。未 opt-in 時不建立自己的 fit-only ResizeObserver、panel MutationObserver或window／ancestor listener，只由一個共用 attribute watcher負責啟用通知。需要取得 instance 時：

```js
const button = document.querySelector("[data-info]");
const control = InfoControl.get(button) || new InfoControl(button);
```

動態卸載的 lifecycle：

```js
control.destroy();
button.remove();
panel.remove();
```

- SPA／動態卡片在移除 trigger 或 panel **之前必須呼叫 `destroy()`**。
- `destroy()` 會移除 click 與當下已啟用的 panel keydown／window resize／clipping ancestor scroll listener，停止 pending animation frame，disconnect fit-only ResizeObserver／panel MutationObserver，清掉由 controller 管理的 `tabindex`／`role`／`data-info-fit-unusable`，並從 instance registry 移除；最後一個 control destroy 後，共用 attribute watcher 也 disconnect。
- Destroy 後 listener／observer 不再觸發；同一 button 之後可用 `new InfoControl(button)` 重新 mount，WeakMap 不會留下舊 instance，也不會重複註冊。
- 純靜態頁在 document 結束時不需逐一 destroy；責任只在 consumer 主動移除或替換 DOM 時發生。

## 6. 元件清單

| 元件名（class） | 用途 | 狀態清單 | 變體清單 | 必要 DOM 結構 | 依賴 token | JS 事件／API |
| --- | --- | --- | --- | --- | --- | --- |
| `.info-stack` | 一組觸發按鈕與解說 panel 的排列容器 | 靜態 | term 預設；`[data-info-kind="judgment"]` | 必須包含 trigger row 與對應 panel；kind 只由 stack 持有 | spacing | 無 |
| `.info-trigger-row` | 水平排列並允許換行的觸發按鈕列 | 靜態 | 一個或多個 button | `.info-stack` 的 child；button 為直接 child | spacing | 無 |
| `.info-button` | 切換一個 term 或 judgment panel | collapsed／expanded／focus | 多個可同時 expanded；fit trigger 可選 | `[data-info][aria-expanded][aria-controls]` | size、space、border、radius、text、surface、accent、focus、motion | `new InfoControl(button)`；click 切換；fit 時條件式同步 panel tabindex；卸載前 `destroy()` |
| `.info-panel` | 承載解說；只有明確 opt-in fit 才提供內部捲動 | hidden／visible；fit 才有 scrollable focus＋region／unusable signal | term 預設；`.info-panel--judgment`；fit trigger owner | `[data-info-panel][id]`，collapsed 初始加 `[hidden]` | space、radius、text、accent、focus | 由對應 control 寫入 `hidden`；fit overflow 且有效可見時同步 tabindex／role；不可用時寫 `data-info-fit-unusable` |
| `.info-panel__line` | judgment panel 內可重複的一行解說 | 靜態 | `data-info-part` 可選 | `.info-panel--judgment` 的 descendant | `--space-2` | 無 |
| `.info-modal-layer`／`.info-modal-scrim` | Shared modal layer與完整host scrim | hidden／open | 一host一份 | layer為backgroundRoot sibling | layer、scrim、motion | `new InfoModalHost(layer, options)` |
| `.info-modal` | Sheet／dialog共用surface | hidden／open | `--sheet`／`--dialog` | role=dialog、aria-modal、title／body／close／action | size、space、radius、surface、shadow | host寫variant並管理focus trap |
| `.info-modal__body` | Clone static Info source並在overflow時成為可聚焦region | empty／content／overflow | static `CLONE_ALLOWED` only | surface descendant | space、text、focus | host同步tabindex／role／aria-labelledby |

## 7. Token 依賴

Runtime 規則實際引用：

- spacing／尺寸：`--space-0`、`--space-1`、`--space-2`、`--space-3`、`--header-height`。
- 色彩：`--color-border`、`--color-text-secondary`、`--color-surface`、`--color-accent`、`--color-accent-weak`、`--color-focus`。
- 圓角與文字：`--radius-sm`、`--radius-pill`、`--text-sm`、`--weight-medium`、`--leading-normal`。
- 轉場：`--duration-fast`、`--duration-base`、`--ease-standard`。
- Modal layer／surface：`--space-4`、`--space-8`、`--space-10`、`--space-12`、`--header-height`、`--z-sheet`、`--color-scrim`、`--color-text`、`--color-bg`、`--color-surface-raised`、`--shadow-md`、`--radius-md`、`--radius-lg`、`--text-md`、`--text-lg`、`--text-xl`、`--weight-bold`、`--leading-tight`。Closed／open opacity是Info domain-local明確`0／1`，不得引用`--opacity-disabled`。
- Fit 可用門檻：Info domain-local `--info-fit-min-visible-block-size`，由 `--text-sm × --leading-normal + --space-3 × 2` 推導。Canonical panel 本來就以同一組 token 設 font-size／line-height，consumer 不得覆寫元件內部值；它不是 `tokens.css` token，也不依賴 `lh` unit。

`info.css` 內的 preview-only scaffolding 另引用 `--space-4`、`--color-text`、`--color-text-tertiary`、`--color-bg`、`--color-border-strong`、`--color-surface-raised`、`--font-sans`、`--text-md`、`--text-xl`、`--shell-width`、`--radius-lg`、`--shadow-md`；這些不增加 runtime `.info-*` 的依賴。

## 驗收與限制

- `confirmed` `info.css` 沒有寫死 hex 或裸 `px`，也沒有新增 token。
- `confirmed` `info.js` 管理 disclosure 狀態；只有 `[data-info-fit-trigger]` 另依實際 overflow 條件同步 panel 的 `tabindex`，不改 Enter／Space 原生觸發、不加動畫或單開互斥。
- `confirmed` `preview-info.html` 並排展示 term／judgment 的 collapsed／expanded 與多行 panel，均可實際展開收合。
- `coverage gap` 本契約不判斷哪些術語需要解說，也不驗收解說文案是否符合消費端 BRIEF／COPY-STYLE。

## 8. Judgment 變體（2026-08-10）

### 8.1 Discriminator 與命名

- `.info-stack[data-info-kind="judgment"]` 是 judgment 的唯一 canonical discriminator owner。
- button 與 panel **不得**重複 `data-info-kind`；一律由 ancestry 判斷，避免同一組控制出現不一致語意。
- Judgment 沒有經驗收的按鈕視覺差異，因此不提供 `.info-button--judgment`；按鈕沿用 `.info-button`。
- `.info-panel--judgment` 只建立多行排列，不新增色彩、字級或其他未經驗收的視覺差異。
- `.info-panel__line` 採 BEM element 命名，因為 line 只屬於 panel，不是獨立 component。

必要 DOM：

```html
<div class="info-stack" data-info-kind="judgment">
  <div class="info-trigger-row">
    <button
      id="judgment-trigger"
      class="info-button"
      type="button"
      data-info="judgment"
      aria-expanded="false"
      aria-controls="judgment-panel"
    >判讀 ℹ</button>
  </div>
  <div
    class="info-panel info-panel--judgment"
    id="judgment-panel"
    data-info-panel="judgment"
    aria-labelledby="judgment-trigger"
    hidden
  >
    <span class="info-panel__line" data-info-part="basis">…</span>
    <span class="info-panel__line" data-info-part="term">…</span>
    <span class="info-panel__line" data-info-part="context">…</span>
  </div>
</div>
```

`data-info-part` 是 consumer 可選的語意 metadata，示例值為 `basis`／`term`／`context`。Design System 不用它判斷完整度，也不要求固定行數或順序；一行、兩行或更多行都合法，產品配方由 BRIEF／COPY-STYLE 驗證。

### 8.2 Token 與狀態

- `.info-panel--judgment` 只以現有 `--space-2` 排列 `.info-panel__line`；不新增 token。
- collapsed／expanded、`aria-expanded`／`hidden`、再次點擊與多開行為完全沿用第 3～5 節。
- `InfoControl` 不讀 `data-info-kind`，所以既有 term DOM 與行為保持相容。

## 9. Fit panel 捲動與可及性（2026-08-10）

### 9.1 明確 opt-in 的高度與捲動能力

基礎 `.info-panel` **不持有** max-size、alignment、overflow 或 overscroll 宣告；未 opt-in consumer 的 computed 基線維持 `max-block-size: none`、`align-self: auto`、`overflow-x/y: visible`，不改 grid stretch，也不在 panel 上建立 wheel／trackpad 捲動死區。

只有同一 stack 的直接 trigger 帶 `[data-info-fit-trigger]`，其直接 panel 才取得 fit 能力：

```css
.info-stack:has(
  > .info-trigger-row > .info-button[data-info-fit-trigger]
) > .info-panel { /* fit-only max/alignment/overflow */ }
```

- Trigger 是既有 JS 與 seam 都使用的 opt-in owner；不另加 panel attribute，可避免 trigger 與 panel 兩份 fit truth 不一致。
- Fit panel 使用 `max-block-size: 100%`、`align-self: start`、`overflow-y: auto`。`align-self: start` 只在 fit 生效，用來避免一般字級 panel 被 grid track 拉成有空白的大片背景。
- 最低可用高度由 panel 上的 `--info-fit-min-visible-block-size: calc(var(--text-sm) * var(--leading-normal) + var(--space-3) * 2)` 單一提供；即一行 canonical `--text-sm × --leading-normal` 加上下 padding。Info JS 與 consumer gate 解析同一 property，不各自寫臨界常數，也不依賴較新的 `lh` unit。
- 若 custom property 因任何原因無法解析成正長度，Info 必須視為 unusable、移除可聚焦 region並寫 failure signal。系統不自動改 layout；consumer gate 必須 FAIL，並由 consumer 成對移除 card／trigger fit attributes。Consumer 覆寫 panel 的 font-size／line-height 不在契約支援範圍；若未遵守，token 推導不承諾跟隨覆寫值。
- `overscroll-behavior-y: contain` 再限縮到 JS 已確認真的 overflow、有效可見且因而帶 `[tabindex="0"][role="region"]` 的 fit panel。一般字級完整可見時不建立 scroll-chain 邊界，wheel 可繼續交給父 Read viewport；只有 panel 確實可捲時才阻止同一手勢提早翻卡。
- Read × Info fit CSS 把 expanded `.info-stack` 分配到**卡片扣除其他保留內容後的剩餘空間**；panel 的 100% 相對該剩餘 track，不相對 `dvh`。系統不偵測剩餘 track 是否可用，也不自動退場；低於 minimum 時由 Info 暴露 failure signal，consumer 必須在自己的不支援格移除 fit attributes。
- Prototype-04 等沒有 `[data-info-fit-trigger]` 的既有 consumer 不命中這些宣告；相容性以九 panel geometry＋wheel A／B gate 驗證，不再只用百分比 max 作推論。

### 9.2 水平軸 policy

Fit panel 只支援**垂直捲動**，不支援水平 scroller：

- `overflow-x: hidden` 明確關閉水平捲動；`min-inline-size: 0`、`max-inline-size: 100%`、`overflow-wrap: anywhere` 套在 fit panel 與其直接 children，讓長文字／不可斷 token 在 panel 寬度內換行。
- Consumer 不得放入要求固定寬度、無法 reflow 的 child；若資料表或第三方 widget 使 `scrollWidth > clientWidth + EPSILON`，組裝 gate 直接 FAIL，不得以水平 scrollbar 當 fallback。

### 9.3 條件式鍵盤可達

所有 `InfoControl` 都可在 lifecycle 中動態進出 fit，但 fit-only 資源只在 button 當下帶 `[data-info-fit-trigger]` 時存在。共用 attribute watcher 發現加入後，controller 建立 panel keydown、window resize、clipping ancestor scroll、ResizeObserver與panel MutationObserver並立即同步；移除後全部拆除，清掉 tabindex／role／failure signal。Cold-start false→true 與 true→false→true 都是正式能力；非 fit instance 不各自常駐 fit-only observers。

1. Panel 必須用 `aria-labelledby` 指向有 id 的 trigger，或自行提供 `aria-label`。
2. Expanded 後先把 panel rect 與**所有 block 軸會 clipping 的 ancestors**取交集；computed `overflow-y`／`overflow` 為 `hidden`、`clip`、`auto`、`scroll` 都算。只看 panel 自身 rect、`clientHeight` 或 `scrollHeight` 不足以證明可見。
3. 有效可見高度必須至少等於 `--info-fit-min-visible-block-size`。因 `getBoundingClientRect()` 位於 transform 後的 visual coordinate space，門檻也要乘同一 block scale；`.device-stage` 50%／65% 不得被誤判不可用。
4. 有效可見高度不足時移除 `tabindex`／`role`，並在 panel 寫入 `data-info-fit-unusable` 作 machine-readable failure signal。恢復可用、collapsed、退出 fit 或 destroy 時移除 signal。
5. 只有有效可見、`scrollHeight > clientHeight + EPSILON` 且 computed `overflow-y` 為 `auto`／`scroll`，才加入 `tabindex="0"` 與 `role="region"`；完整可見或不 overflow 時不增加 Tab stop／landmark。
6. Focus outline 沿用 `--color-focus`／`--space-1`；聚焦後由瀏覽器原生 Arrow／PageUp／PageDown／Home／End 捲動。`InfoControl` 只對這些鍵執行 `stopPropagation()`，避免事件冒泡後觸發父層 Read 翻卡，不呼叫 `preventDefault()`、不自行計算 scrollTop。
7. Fit 啟用期間的 `ResizeObserver`、panel `MutationObserver`、window resize 與 clipping ancestor scroll 會重算 focusability／failure signal，以涵蓋字級、容器、動態文字、orientation 與長卡捲動；button attribute 的共用 MutationObserver只負責動態啟停，不取代 visibility observers。

### 9.4 Read seam limitation

Info 只提供 panel 的 overflow、有效可見判斷、failure signal 與條件式 focusability，不接管父卡高度，也不決定哪個 sibling 可收起。Read × Info fit 的 card／trigger opt-in pair、`data-info-collapse-on-open`、多開、consumer 退場與有效可見度 gate 由 `components/read/contract.md` 的追加節負責；`ReadSwipe` JS 不知道 Info，也不會自動移除 fit。

Consumer 不得把理解判讀所需的唯一證據、目前 focus 所在區域或必要操作標成 `[data-info-collapse-on-open]`。收起哪一區是 BRIEF 的產品優先序，不是 Info 的元件預設。

## 10. Modal presentation（2026-08-11）

> 本節試行「契約／可變／示意」標示。Modal carrier已收進Info domain；產品文案、直點trigger視覺與judgment／term對presentation的mapping沒有一起收。

### 10.1 契約

#### Presentation 與owner

- Modal trigger必須是native`button[type="button"]`，以`data-info-presentation="sheet|dialog"`逐顆opt in；值不得由`data-info-kind`推導。
- Trigger必須提供`data-info`、`data-info-modal-target`、`data-info-modal-title`、`aria-haspopup="dialog"`、`aria-expanded="false"`與`aria-controls`。`aria-controls`指向實際shared surface；source由`data-info`對`data-info-panel`唯一配對。
- Modal trigger不得帶`data-info-fit-trigger`；只含modal Info的card不得借用`data-read-info-fit`。Read × Info fit仍是受支援、非legacy的inline variant，沒有被本節移除或標deprecated。
- `InfoControl`仍是一顆trigger的owner：inline維持原本多開；modal委派給host。`InfoModalHost`是一個host scope的shared surface與singleton owner；開第二顆只把前一顆`aria-expanded`改false，不回焦前一顆。
- Composition唯一的host介面是`new InfoModalHost(layer, { backgroundRoot, fallbackFocus })`。兩個elements只從constructor取得；不解析`data-info-inert-target`或其他第二訊號。Layer parent是bounded carrier的positioning host，computed `position`不得為`static`；layer與backgroundRoot必須是其直接siblings。
- Scrim與surface必須是兩個不同elements，且各自為layer的直接子元素；兩者因此互不包含且互為siblings。不得讓同一element兼任兩角色、把surface放進`aria-hidden="true"`的scrim，或把scrim縮進surface。Surface id與title id都必須non-empty、document-wide unique，且不得被另一個mounted host重用；`destroy()`後只有在舊duplicate DOM也已移除時才可由其他host沿用。
- `backgroundRoot`必須包含該host的全部non-modal interactive background，包括每顆modal trigger、各自source與fallbackFocus。Constructor與每次open都掃positioning host**（含 host 元素本身，不只後代）**；root／layer以外仍有visible native interactive（含 `audio[controls]`／`video[controls]`／`object`／`embed`／`area[href]`）、focusable或interactive role即FAIL。**判定「已排除」只認 `hidden`／`inert`／`display:none`／`visibility:hidden`／無 layout rect——不認 `aria-hidden="true"`**：它只影響accessibility exposure，不會取消 focus、click 或鍵盤操作。Trigger或source不在root內也直接FAIL，不能只靠視覺scrim假裝modal。
- `fallbackFocus`必須是backgroundRoot內可見、connected的native button。
- 本輪不支援nested modal。Constructor／open遇到原本已inert的backgroundRoot直接FAIL，不改寫其他owner狀態。

#### Clone 安全

`InfoModalHost.CLONE_ALLOWED`是static與runtime共用白名單：

- Elements：`p／span／strong／em／br／small／code／ul／ol／li`。
- Attributes：`class／data-info-part`。
- Text nodes可保留；其他node type、所有名單外element／attribute與全部`on*`都FAIL。
- 白名單只檢查source的descendants；`.info-panel[data-info-panel]` source root本身不被clone。名單要擴充必須先有新來源形狀證據。

#### 幾何與motion

- `.info-modal-layer`與`.info-modal-scrim`完整覆蓋定位host。Pre-open closed opacity明確為`0`，`.is-open`為`1`；modal runtime不得直接或透過alias引用`--opacity-disabled`。
- `.info-modal--sheet`的inline size等於host 100%，底邊貼host；上兩角為`--radius-lg`、下兩角為0。Minimum block size為`--space-12 × 8`，現行解析值384 layout units；maximum為host block size減`--space-12`。
- `.info-modal--dialog`在host水平／垂直置中；inline size為`min(host inline size - --space-8, --space-12 × 7)`，現行maximum 336 layout units；maximum block size為host block size減`--space-8`，四角都是已驗收來源的`--radius-lg`。
- Layer opacity使用`--duration-fast`；sheet transform使用現有`--duration-base`，不得使用不存在的`--duration-normal`。Normal motion關閉時先移除`.is-open`，等待layer／surface的longest computed transition完成後才設`hidden`；`transitionend`未發生時，timeout fallback使用同一批token解析出的computed duration，不寫死毫秒。`prefers-reduced-motion: reduce`時layer與sheet transition都為none並立即hidden。
- `.info-modal__body`只提供垂直overflow；若`scrollHeight > clientHeight + 1`，host加入`tabindex="0" role="region" aria-labelledby="<title-id>"`，恢復不overflow或close時移除。Body focus時由host以computed line-height／clientHeight處理Arrow／Page／Home／End，不使用寫死捲動距離，也不讓按鍵落到背景。

#### Focus、close與lifecycle

- Surface固定`role="dialog" aria-modal="true"`並由自己的title命名。Sheet初始focus到close；dialog初始focus到action。
- Tab／Shift+Tab留在surface。ESC、scrim、sheet close與dialog action四條路徑共用同一個`close()`：立即把active trigger expanded改false、解除background inert並回焦；normal motion期間layer維持rendered但移除`.is-open`，同時以`inert`＋`aria-hidden="true"`退出互動與accessibility tree，transition完成才hidden並清空clone。Closing期間再次open會被忽略，直到完成hidden後才可重開。
- Trigger已移除／不可focus時，立即回constructor的`fallbackFocus`，不等待close transition。Modal control在open期間`destroy()`時也走fallback；host`destroy()`則取消pending transition listener／timer並立即hidden，在trigger仍有效時回trigger，否則回fallback，再移除layer／document／window listeners、ResizeObserver與registry。
- SPA移除modal trigger前先`InfoControl.destroy()`；移除整個composition前，先destroy所有controls，再`InfoModalHost.destroy()`。

### 10.2 可變

- Consumer逐trigger選sheet或dialog，也逐trigger提供title；judgment／term、文案類型與`data-info-part`不決定presentation。
- Sheet實際block size可隨static內容在384 minimum與host maximum間成長；dialog實際block size由內容決定但不得超過maximum。384／336公式、alignment與radius不是consumer可變值。
- Body內容、行數、順序與是否套`.info-button`視覺由consumer BRIEF決定，但clone descendants仍受白名單約束。Slice-05B的直點trigger樣式不屬本契約。

### 10.3 示意

```html
<div class="consumer-info-modal-scope" style="position: relative">
<div id="info-background">
  <button id="info-fallback" type="button">返回上一層</button>
  <button
    class="info-button"
    type="button"
    data-info="sample"
    data-info-presentation="sheet"
    data-info-modal-target="info-modal-host"
    data-info-modal-title="解說"
    aria-haspopup="dialog"
    aria-expanded="false"
    aria-controls="info-modal-surface"
  >開啟解說 ℹ</button>
  <div class="info-panel" data-info-panel="sample" hidden>
    <span class="info-panel__line" data-info-part="term">…</span>
  </div>
</div>

<div class="info-modal-layer" id="info-modal-host" data-info-modal-host hidden>
  <div class="info-modal-scrim" data-info-modal-scrim aria-hidden="true"></div>
  <section class="info-modal" id="info-modal-surface" data-info-modal
    role="dialog" aria-modal="true" aria-labelledby="info-modal-title" tabindex="-1">
    <header class="info-modal__header">
      <h2 class="info-modal__title" id="info-modal-title" data-info-modal-title>解說</h2>
      <button class="info-modal__close" type="button" data-info-modal-close aria-label="關閉">×</button>
    </header>
    <div class="info-modal__body" data-info-modal-body></div>
    <button class="info-modal__action" type="button" data-info-modal-action>我知道了</button>
  </section>
</div>

</div>

<script src="components/info/info.js"></script>
<script>
  const host = new InfoModalHost(document.getElementById("info-modal-host"), {
    backgroundRoot: document.getElementById("info-background"),
    fallbackFocus: document.getElementById("info-fallback")
  });
</script>
```

此示意只證明consumer明確選sheet與constructor wiring，不代表judgment預設sheet，也不收直點trigger視覺。

### 10.4 標示試行與coverage

「契約／可變／示意」在本次實作有用：它把geometry、singleton、clone與focus寫成可FAIL命令，同時把presentation mapping與文案留在consumer；示意不再被誤讀成產品default。限制是證據狀態仍須另外寫，三種標示不能把未測能力變成confirmed。

本輪public contract只承諾單一host composition；catalog的isolated no-Shell與測試用雙host只是Chromium fixture，no-Shell與同頁多host維持proposed，不寫成跨consumer已支援。ARIA relation已完成DOM migration方案，但目標screen reader尚未驗證。瀏覽器驗證只涵蓋Chromium；Safari、Firefox與一般行動瀏覽器（iOS Safari、Android Chrome）未驗證。
