diff --git a/.claude/skills/architecture/SKILL.md b/.claude/skills/architecture/SKILL.md
index 7831a1f..844f04d 100644
--- a/.claude/skills/architecture/SKILL.md
+++ b/.claude/skills/architecture/SKILL.md
@@ -44,5 +44,5 @@ description: VimAction 프로젝트의 현재 아키텍처·구조(최종 상태
| 포커스 리졸버·디스패치 읽기 | 계열 캐시·갱신 트리거, 게시 큐 위 창 읽기(`FocusedText`)와 파생 질의·소비 지점 | [focus-and-dispatch-reads.md](references/focus-and-dispatch-reads.md) |
| 재진입과 안전장치 | 합성 이벤트 마커 불변식(무한 루프 방지), 단일 ActionExecutor, 킬스위치·중단 래치·원자 그룹 | [reentrancy-and-safety.md](references/reentrancy-and-safety.md) |
| 프로파일과 설정 | YAML(Yams) 설정 시딩·스키마, 앱별 프로파일, 수동 리로드, UserDefaults 경계 | [profiles-and-config.md](references/profiles-and-config.md) |
-| 앱 셸 | 메뉴바·설정 창 3탭·Dock 아이콘 수명·로그인 자동 시작·Sparkle·온보딩 push | [app-shell.md](references/app-shell.md) |
+| 앱 셸 | 메뉴바·설정 창 4탭·Dock 아이콘 수명·로그인 자동 시작·Sparkle·온보딩 push | [app-shell.md](references/app-shell.md) |
| 온스크린 모드 인디케이터 | 모드 전환 시 오버레이 라벨 — 콜백 훅·전용 읽기 큐·앵커 사다리(요소→창)·순수 레이아웃·비활성화 패널·메뉴바 표시 사다리 재사용 | [mode-indicator-overlay.md](references/mode-indicator-overlay.md) |
diff --git a/.claude/skills/architecture/references/app-shell.md b/.claude/skills/architecture/references/app-shell.md
index 4b47df5..246a00b 100644
--- a/.claude/skills/architecture/references/app-shell.md
+++ b/.claude/skills/architecture/references/app-shell.md
@@ -1,6 +1,6 @@
# 앱 셸
-- **Last updated**: 2026-09-04 (릴리스 노트 노출 배선 반영)
+- **Last updated**: 2026-09-15 (설정 창 Indicator 탭 — 4탭)
## 현재 구조
@@ -18,9 +18,9 @@
열림 훅의 앱측 진입점은 `AppState.settingsWindowDidAppear()`이고 reader와 함께 **`TabView` 루트에 붙는다** — 탭 하나 안에 두면 다른 탭이 기본으로 열릴 때 훅이 뜨지 않는다. 이 메서드는 Dock 훅·온보딩 마무리·로그인 항목 상태 재조회를 겸하므로 **멱등**이다.
-### 설정 창 — `TabView` 3탭, 460×560 고정
+### 설정 창 — `TabView` 4탭, 460×560 고정
-**General**(권한 상태 + Behavior[로그인 시 자동 시작·Normal 탈출·온스크린 모드 인디케이터] + Updates 토글 + 접힌 `DisclosureGroup` "Diagnostics") / **Apps**(설정 파일 상태 — [profiles-and-config.md](profiles-and-config.md)) / **About**(버전 + 업데이트 확인 + 링크). 탭 순서는 General이 먼저지만 **기본 선택은 Apps**이고, 권한 섹션은 미허용이면 General 최상단·허용되면 아래쪽(Behavior·Updates 뒤)에 한 줄로 자리를 옮긴다. 높이를 명시하지 않으면 greedy한 grouped `Form` 때문에 창이 임의 높이로 굳는다 ([20260809_settings-window-three-tabs.md](../../decisions/references/20260809_settings-window-three-tabs.md)).
+**General**(권한 상태 + Behavior[로그인 시 자동 시작·Normal 탈출] + Updates 토글 + 접힌 `DisclosureGroup` "Diagnostics") / **Indicator**(탭 아이콘 `text.cursor`, 섹션 하나 — 온스크린 모드 인디케이터 토글과 그 아래 "Show" Picker(전환 시만 / Normal·Visual 동안도 — 토글이 꺼지면 `.disabled`)·"Indicator style" Picker(배지 / 창 테두리 / 화면 테두리 — 토글이 꺼지거나 상시 표시가 없으면 `.disabled`), 값·영속은 `ModeIndicatorController` 소유이고 뷰는 바인딩만, [mode-indicator-overlay.md](mode-indicator-overlay.md)) / **Apps**(설정 파일 상태 — [profiles-and-config.md](profiles-and-config.md)) / **About**(버전 + 업데이트 확인 + 링크). 탭 순서는 General이 먼저지만 **기본 선택은 Apps**이고, 권한 섹션은 미허용이면 General 최상단·허용되면 아래쪽(Behavior·Updates 뒤)에 한 줄로 자리를 옮긴다. 높이를 명시하지 않으면 greedy한 grouped `Form` 때문에 창이 임의 높이로 굳는다 ([20260809_settings-window-three-tabs.md](../../decisions/references/20260809_settings-window-three-tabs.md), [20260915_settings-window-indicator-tab.md](../../decisions/references/20260915_settings-window-indicator-tab.md)).
### 로그인 시 자동 시작 — `SMAppService.mainApp`
diff --git a/.claude/skills/architecture/references/mode-indicator-overlay.md b/.claude/skills/architecture/references/mode-indicator-overlay.md
index cc3bf0b..4229548 100644
--- a/.claude/skills/architecture/references/mode-indicator-overlay.md
+++ b/.claude/skills/architecture/references/mode-indicator-overlay.md
@@ -1,76 +1,86 @@
# 온스크린 모드 인디케이터 (오버레이)
-- **Last updated**: 2026-09-06
+- **Last updated**: 2026-09-15
## 현재 구조
-화면의 라벨 알약은 **두 겹**이다. 모드가 바뀌면 **순간 표시**(flash)가 포커스 요소 근처에 약 1초 뜨고 페이드아웃하며, **Insert가 아닌 동안**에는 한 단계 작은 **상시 배지**가 같은 자리에 계속 붙어 있는다 (Insert는 배지 없음). 둘 다 Settings General 탭 토글 하나로 꺼진다. 갱신은 **이벤트 기반만**이다 — 타이머 폴링도, 키마다 재배치도 없다.
+화면의 표시는 **두 겹**이다. 모드가 바뀌면 **순간 표시**(flash)가 **캐럿 아래**(캐럿이 없으면 포커스 요소 근처)에 약 1초 뜨고 페이드아웃하며, **Insert가 아닌 동안**에는 **상시 표시**가 계속 남는다 (Insert는 상시 표시 없음). 상시 표시는 Settings에서 끌 수도 있고("Only when the mode changes" — 그러면 flash만 남는다) 형태도 고른다 — 기본은 요소 모서리의 한 단계 작은 **배지**, 대체 스타일은 포커스 창 rect를 두르는 **창 테두리**와 포커스 창이 있는 디스플레이를 두르는 **화면 테두리**(둘 다 강조색 프레임 + 오른쪽 위 모서리 라벨). 어느 스타일이든 flash는 캐럿/요소 사다리를 탄다. 전부 Settings Indicator 탭 토글 하나로 꺼진다. 갱신은 **이벤트 기반만**이다 — 타이머 폴링도, 키마다 재배치도 없다.
```mermaid
graph LR
T1["① EventTapController.updateMode
(탭 콜백 동기 구간)"] --> R
T2["② FocusedElementResolver
포커스·앱 활성화·창 이동/리사이즈"] --> R
T3["③ AppState.trackIndicatorLadder
(withObservationTracking 재무장)"] --> R
- T4["④ isEnabled.didSet (Settings 토글)"] --> R
+ T4["④ isEnabled·isPersistentEnabled·style.didSet (Settings)"] --> R
T5["⑤ didChangeScreenParameters"] --> R
R["ModeIndicatorController.reconcile
presentation(순수) + token·pending 코얼레싱"]
- R -->|전용 큐 .utility| G["ModeIndicatorGeometryReader.read(pid)
요소 rect → (없으면) 창 rect"]
- G -->|rect만 메인 홉| L["ModeIndicatorLayout.panelFrame
사다리·배치·flip·클램프 (순수)"]
- L --> P["flash 패널 / 배지 패널
비활성화 NSPanel 2개"]
+ R -->|전용 큐 .utility| G["ModeIndicatorGeometryReader.read(pid, includesCaret, includesWindow)
요소 rect → (없거나 창 테두리면) 창 rect, flash면 캐럿 rect"]
+ G -->|rect만 메인 홉| L["ModeIndicatorLayout (순수)
panelFrame(ladder) · windowBorderLayout · screenBorderLayout"]
+ L --> P["flash 패널 / 배지 패널 / 테두리 패널
비활성화 NSPanel 3개"]
```
| 구성 요소 | 격리 | 역할 |
|---|---|---|
-| `ModeIndicatorController` (`AppState` 소유) | `@MainActor @Observable` | 다섯 트리거의 합류점. 표시 판정은 순수 `presentation(isEnabled:inputs:)`, 조율은 `reconcile(flashes:rereadGeometry:)` 하나. `pending` 한 칸이 최신 요청만 들고 읽기는 동시에 하나이며, 표시를 바꾸는 트리거가 `token`을 올려 늦게 착지한 읽기를 버린다. 설정 토글(`isEnabled`)의 소유자이자 전용 큐 `dev.pilyang.VimAction.mode-indicator-geometry`(`.utility`)의 소유자 |
-| `ModeIndicatorGeometryReader` | `nonisolated` | pid만 받아 `AXRead.focusedElement` → `AXPosition`+`AXSize`, 요소 rect가 쓸 만하지 않을 때만 `AXFocusedWindow`. 50ms 타임아웃은 `AXRead` 상속 |
-| `ModeIndicatorLayout` | `nonisolated` 순수 | 앵커 사다리(요소 → 창 → 없음, 면적 있는 rect만) → 배치(요소 단은 요소 바깥 오른쪽 위 4pt, 창 단은 창 안쪽 오른쪽 위 12/6pt) → AX→AppKit flip → 클램프. 화면은 호출자가 `Screen`(frame + visibleFrame) 배열로 넘긴다 |
-| `ModeIndicatorPanel` × 2 | `@MainActor` | `NSPanel(.borderless, .nonactivatingPanel)`, `ignoresMouseEvents`, `hidesOnDeactivate = false`, `collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary, .ignoresCycle]`. `ModeIndicatorStyle`이 폰트·여백·radius·창 층을 가른다: flash는 bold 12pt/`.statusBar`, 배지는 semibold 10pt/`.statusBar - 1`. `flash`는 0.15/0.7/0.3s 페이드, `show`는 애니메이션도 타이머도 없다. 각각 첫 표시에서 lazy 생성 |
+| `ModeIndicatorController` (`AppState` 소유) | `@MainActor @Observable` | 다섯 트리거의 합류점. 표시 판정은 순수 `presentation(isEnabled:isPersistentEnabled:style:inputs:)`, 조율은 `reconcile(flashes:rereadGeometry:)` 하나. `pending` 한 칸이 최신 요청만 들고 읽기는 동시에 하나이며, 표시를 바꾸는 트리거가 `token`을 올려 늦게 착지한 읽기를 버린다. 설정 셋(`isEnabled`·`isPersistentEnabled`·`style: ModeIndicatorPresentationStyle`)의 소유자이자 전용 큐 `dev.pilyang.VimAction.mode-indicator-geometry`(`.utility`)의 소유자. 상시 표시 패널의 숨김은 `hidePersistentPanels(keeping:)` 한 자리이고, 테두리 패널이 지금 어느 스타일로 떠 있는지(`borderPanelStyle`)를 기억해 창↔화면 테두리 전환도 이전 형태를 즉시 감춘다 |
+| `ModeIndicatorGeometryReader` | `nonisolated` | pid만 받아 `AXRead.focusedElement` → `AXPosition`+`AXSize`, 요소 rect가 쓸 만하지 않거나 **`includesWindow`(= 창 테두리 스타일의 상시 표시)** 일 때 `AXFocusedWindow`(한 번만). **`includesCaret`(= 요청에 flash가 있음)일 때만** 캐럿 rect를 추가로 읽는다 — 순서는 `AXSelectedTextRange`의 `loc`으로 `AXBoundsForRange (loc,1)` → 안 되고 `loc>0`이면 `(loc-1,1)` → `AXSelectedTextMarkerRange`(불투명 CFType, 변환 없음)+`AXBoundsForTextMarkerRange`, 첫 쓸 만한 것에서 멈춘다. 길이 0 범위는 쓰지 않고 `AXNumberOfCharacters`도 미리 묻지 않는다. 50ms 타임아웃은 `AXRead` 상속 |
+| `ModeIndicatorLayout` | `nonisolated` 순수 | 앵커 사다리는 **묻는 알약에 따라 첫 단이 다르다**(`Ladder`): flash는 `caretFirst`(캐럿 → 요소 → 창 → 없음), 상시 표시는 `elementFirst`(요소 → 창 → 없음). 면적 있는 rect만 한 단이고, 캐럿의 유효성(`isUsableCaret`)도 여기 소유 — 높이 > 0(폭 0 허용), 요소 rect와 다름, 요소 rect가 쓸 만하면 그 안에 걸침. 배치는 캐럿 단 캐럿 아래 왼쪽 정렬 4pt / 요소 단 요소 바깥 오른쪽 위 4pt / 창 단 창 안쪽 오른쪽 위 12/6pt → AX→AppKit flip → 클램프. `screenBorderLayout`은 상시 사다리의 앵커가 속한 화면의 `visibleFrame`과 그 안 오른쪽 위 라벨 프레임(인셋 = 선 굵기 4 + 8)을, `windowBorderLayout`은 사다리 없이 창 rect 자체(flip만, 클램프 없음)와 같은 규칙의 라벨을 낸다 — 라벨은 앵커 화면 `visibleFrame`으로 민 뒤 창 frame 안으로 한 번 더 민다(라벨보다 좁은 창). 창이 없거나 면적이 없으면 nil(폴백 없음). 화면은 호출자가 `Screen`(frame + visibleFrame) 배열로 넘긴다 |
+| `ModeIndicatorPanel` × 2 | `@MainActor` | flash·배지 알약. `ModeIndicatorStyle`이 폰트·여백·radius·창 층을 가른다: flash는 bold 12pt/`.statusBar`, 배지는 semibold 10pt/`.statusBar - 1`. `flash`는 0.15/0.7/0.3s 페이드, `show`는 애니메이션도 타이머도 없다. 각각 첫 표시에서 lazy 생성 |
+| `ModeIndicatorBorderPanel` × 1 | `@MainActor` | 창·화면 테두리 공용 — 호출자가 준 frame(창 rect 또는 디스플레이의 `visibleFrame`)을 덮는 패널 **하나**에 강조색 테두리(`ModeIndicatorBorderView`, 모서리 반경은 선 굵기의 2배로 두 스타일 공통)와 모서리 라벨(`ModeIndicatorPillView(style: .badge)` 서브뷰)을 함께 그린다. 층은 배지와 같다. `show`/`hide`는 배지와 같은 계약(멱등, 무애니메이션). 테두리 스타일을 고른 뒤 첫 표시에서 lazy 생성. 두 스타일이 인스턴스 하나를 나눠 쓰고 어느 형태인지는 컨트롤러가 기억한다 |
+| `ModeIndicatorPanel.makeOverlayPanel(level:)` | `@MainActor` | 비활성화 패널 설정의 **유일한** 생성 지점 — `NSPanel(.borderless, .nonactivatingPanel)`, `ignoresMouseEvents`, `hidesOnDeactivate = false`, `collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary, .ignoresCycle]`. 알약 둘과 테두리가 전부 여기서 받는다 |
-라벨 문자열은 `Mode.overlayLabel`, 배지 표시 여부는 `Mode.showsPersistentBadge`(Insert만 false)로 둘 다 `AppState.swift`에 있다.
+라벨 문자열은 `Mode.overlayLabel`, 모드별 상시 표시 여부는 `Mode.showsPersistentBadge`(Insert만 false)로 둘 다 `AppState.swift`에 있다 — 설정의 상시 표시 off는 판정(`presentation`)에서 이 값에 AND될 뿐 모드 속성을 건드리지 않는다. 스타일 enum `ModeIndicatorPresentationStyle`(`badge`/`windowBorder`/`screenBorder`, raw 문자열이 UserDefaults 값, `usesBorderPanel`이 테두리 둘을 묶는다)은 `ModeIndicatorController.swift`에 있다 — 알약 외양 enum `ModeIndicatorStyle`(flash/badge)과는 다른 축이다.
### 트리거 다섯
| # | 계기 | 경로 | 하는 일 |
|---|---|---|---|
-| ① | 모드 전환 | `updateMode` → `onModeChange` → `modeDidChange` | flash + 배지, 항상 읽는다 |
-| ② | 포커스 요소 변경·앱 활성화·창 이동·리사이즈 | `FocusedElementResolver.onFocusGeometryChanged` → (`EventTapController` 통로) → `anchorDidChange` | 배지가 보여야 할 때만 읽는다 |
+| ① | 모드 전환 | `updateMode` → `onModeChange` → `modeDidChange` | flash + 상시 표시, 항상 읽는다 (캐럿 포함) |
+| ② | 포커스 요소 변경·앱 활성화·창 이동·리사이즈 | `FocusedElementResolver.onFocusGeometryChanged` → (`EventTapController` 통로) → `anchorDidChange` | 상시 표시가 보여야 할 때만 읽는다 (캐럿 없이) |
| ③ | 사다리 변화 (탭 고장·마스터 off·킬스위치·앱별 disabled·Secure Input·설정 리로드) | `AppState.trackIndicatorLadder()`의 `withObservationTracking` 재무장 루프 → `stateDidChange` | 표시할 것이 실제로 달라졌을 때만 읽는다 |
-| ④ | Settings 토글 | `isEnabled.didSet` | 마지막 입력으로 다시 판정 (off는 즉시 숨김, on은 즉시 복귀) |
+| ④ | Settings 토글·상시 표시·스타일 | `isEnabled.didSet`·`isPersistentEnabled.didSet`·`style.didSet` | 마지막 입력으로 다시 판정 (off는 즉시 숨김, on은 즉시 복귀; 상시 표시 off는 `showsBadge`가 접혀 Insert와 같은 경로 — 앵커 이벤트에 AX 왕복 0건; 스타일 변경은 `Presentation`에 실린 스타일로 `desired != current`가 되어 상시 표시가 있으면 다시 읽어 새 형태로 그리고, 이전 형태는 즉시 감춘다) |
| ⑤ | 디스플레이 재구성 | `NSApplication.didChangeScreenParametersNotification` → `anchorDidChange` | ②와 같다 |
## 불변식·계약
- **훅은 탭 콜백의 동기 구간에서 불린다.** 거기서 하는 일은 판정(순수)·`pending` 대입·`queue.async` 1회까지다. AX 호출과 `NSScreen` 조회는 전부 큐/메인 홉 뒤다 — 어기면 콜백 경량 불변식이 깨진다.
-- **AX는 전용 큐에서만, pid만 건너간다.** `AXUIElement`는 큐 경계를 넘지 않고 돌아오는 것은 rect뿐이다. **리졸버 `readQueue`를 재사용하지 않는다** — 콜드 앱에서 이 읽기는 AX 호출 6회×50ms까지 걸릴 수 있고, 리졸버 큐는 키 디스패치가 의존하는 포커스 계열 캐시를 먹인다.
-- **표시 조건은 메뉴바 사다리와 동일하다.** `MenuBarIndicator.resolve`가 `.mode`가 아니면 즉시 두 패널 모두 `hide()`. 읽는 도중 사다리를 벗어나면 토큰 불일치로 그 읽기를 버린다 — "가로채지 않는데 NORMAL"을 띄우면 안 된다.
-- **사다리 관찰 루프는 레벨 트리거다.** `onChange`는 willSet에서(킬스위치 경로면 메인 밖에서도) 불리므로 거기서는 홉만 걸고, 재무장이 **현재 값**을 다시 읽어 수렴한다. 그래서 `reconcile`은 직전 이벤트가 아니라 **자기 현재 표시(`current`)와만** 비교한다. 관찰 블록은 값만 만들고 부수효과가 없어야 한다 — 컨트롤러 메서드를 그 안에서 부르면 컨트롤러 내부 상태까지 추적돼 읽기가 끝날 때마다 루프가 스스로를 깨운다 (그래서 `isEnabled` 외 저장 프로퍼티는 전부 `@ObservationIgnored`).
-- **flash 요청은 `pending`과 따로 산다.** 같이 담으면 읽는 사이에 온 앵커·사다리 이벤트가 `pending`을 덮어써 정당한 flash가 사라진다(Cmd-Tab 직후 Esc). 다만 붙일 앵커가 없어 그리지 못한 경우에는 **버린다** — 들고 있으면 한참 뒤 앵커 이벤트에 얹혀 살아나 엉뚱한 때 번쩍인다.
+- **AX는 전용 큐에서만, pid만 건너간다.** `AXUIElement`는 큐 경계를 넘지 않고 돌아오는 것은 rect뿐이다. **리졸버 `readQueue`를 재사용하지 않는다** — 콜드 앱에서 이 읽기는 flash면 캐럿 변형까지 AX 호출 11회×50ms까지 걸릴 수 있고, 리졸버 큐는 키 디스패치가 의존하는 포커스 계열 캐시를 먹인다.
+- **flash는 캐럿부터, 상시 표시는 요소부터 시작하고 상시 표시는 캐럿을 따라다니지 않는다.** 캐럿은 flash가 있는 요청(`pendingFlash`)에서만 읽으며 그 필요는 `pump()`가 `includesCaret`으로 리더에 명시적으로 넘긴다 — 앵커·사다리 이벤트의 재배치는 캐럿 왕복 없이 기존 비용에 머문다. flash를 세우는 유일한 경로(`modeDidChange`)가 항상 토큰을 올려 새 읽기를 띄우므로 캐럿 없이 띄운 읽기로 flash가 그려지는 일은 없다. 예산: flash 한 번에 최대 AX 호출 5회가 더 든다. **창 rect도 같은 규칙이다** — `includesWindow`는 `showsBadge && style == .windowBorder`(소비자 조건 그대로)일 때만 참이라 Insert·순간 표시만인 요청은 창 왕복 셋을 내지 않고, `inFlight.request == desired` 비교에 스타일이 실려 있어 창 없이 띄운 읽기가 창 테두리 요청에 재사용되는 일은 없다.
+- **캐럿 유효성은 순수 계층(`isUsableCaret`) 한 벌이다.** 리더가 다음 변형을 시도할지도 같은 술어로 판정한다 — 두 벌이면 리더가 넘긴 것을 사다리가 버린다. `(loc-1,1)`이 줄 첫머리에서 개행 문자라 rect가 이전 줄 끝에 오는 quirk는 수용하고 특별 취급하지 않는다. Chromium 브라우저(Chrome·Arc)는 캐럿 변형이 전부 빈 값이라 조용히 요소 단으로 내려간다 — `AXEnhancedUserInterface`를 세우지 않는다.
+- **표시 조건은 메뉴바 사다리와 동일하다.** `MenuBarIndicator.resolve`가 `.mode`가 아니면 즉시 세 패널 모두 `hide()`. 읽는 도중 사다리를 벗어나면 토큰 불일치로 그 읽기를 버린다 — "가로채지 않는데 NORMAL"을 띄우면 안 된다.
+- **상시 표시의 숨김 규칙은 스타일 불문 한 자리다** — `hidePersistentPanels(keeping:)`가 유일한 숨김 지점이고, 배지에 적용되는 모든 사유(Insert·사다리 이탈·토글 off·pid 없음·자기 pid·앵커 없음·스타일 변경)가 테두리에도 같이 적용된다. 스타일이 바뀌면 이전 형태의 패널은 `reconcile`에서 즉시 감춰지고 새 형태는 읽기가 착지한 뒤 그려진다. 테두리 패널은 창·화면 테두리가 나눠 쓰므로 `keeping`이 테두리라도 지금 떠 있는 형태(`borderPanelStyle`)와 다르면 감춘다 — 창↔화면 테두리 전환에도 같은 계약이다.
+- **자기 pid 경로가 곧 설정 창의 경로다.** 설정 창이 열려 있으면(메뉴바 아이콘 클릭도 같다) VimAction이 최전면이라 리졸버가 우리 pid에 붙어 있고, `reconcile`은 읽지도 `current`를 건드리지도 않는다 — 다만 거짓말은 지우고(Insert면 상시 표시 숨김, 스타일·상시 표시 설정이 바뀌면 이전 형태 숨김) **떠 있던 읽기는 무효화한다**(토큰 증가·`pending` 비움·밀린 flash 만료 — 이전 앱을 향해 떠 있던 읽기가 착지해 방금 감춘 것을 되살리면 안 된다; `inFlight`는 `finish`만 비운다). 그래서 Picker의 새 형태는 사용자가 앱으로 돌아가는 앵커 이벤트(`anchorDidChange` — 앱 활성화 알림은 pid가 같아도 온다)에서 그려진다 (토글을 켤 때와 같은 수렴). `stateDidChange`만으로 복귀하는 경로가 생기면 `current`가 이미 그 값이라 다시 그리지 않으므로 깨진다.
+- **사다리 관찰 루프는 레벨 트리거다.** `onChange`는 willSet에서(킬스위치 경로면 메인 밖에서도) 불리므로 거기서는 홉만 걸고, 재무장이 **현재 값**을 다시 읽어 수렴한다. 그래서 `reconcile`은 직전 이벤트가 아니라 **자기 현재 표시(`current`)와만** 비교한다. 관찰 블록은 값만 만들고 부수효과가 없어야 한다 — 컨트롤러 메서드를 그 안에서 부르면 컨트롤러 내부 상태까지 추적돼 읽기가 끝날 때마다 루프가 스스로를 깨운다 (그래서 `isEnabled`·`isPersistentEnabled`·`style` 외 저장 프로퍼티는 전부 `@ObservationIgnored`).
+- **flash 요청은 `pending`과 따로 산다.** 같이 담으면 읽는 사이에 온 앵커·사다리 이벤트가 `pending`을 덮어써 정당한 flash가 사라진다(Cmd-Tab 직후 Esc). 다만 붙일 앵커가 없어 그리지 못한 경우에는 **버린다** — 들고 있으면 한참 뒤 앵커 이벤트에 얹혀 살아나 엉뚱한 때 번쩍인다. flash 사다리가 상시 사다리를 포함하므로 "flash 프레임 없음 = 앵커 없음"이고, 그 반대(캐럿만 있고 요소·창이 없음)는 flash만 뜨고 상시 표시는 감춘다.
- **아무것도 안 바뀐 reconcile은 토큰을 올리지 않고, 같은 상태를 같은 토큰으로 이미 읽는 중이면 새 읽기를 띄우지 않는다**(`needsGeometryRead`가 그 판정의 순수 소유자다). 둘 다 같은 사고를 막는다: 토큰이 올라가면 진행 중인 읽기가 폐기된다. 후자가 없으면 **모드 전환마다 읽기가 두 번 난다** — 사다리 관찰 루프의 재무장이 `mode`의 willSet에서 예약돼 전환 읽기가 착지하기 전에 뒤따라 오는데, 그때 밀린 flash 때문에 판정이 참이 되기 때문이다.
-- **패널은 절대 앱을 활성화하지 않는다.** `orderFrontRegardless()`만 쓰고 `makeKey…`·`NSApp.activate` 계열은 없다. 활성화되면 `FrontmostAppGate`의 최전면 캐시가 자기 자신으로 덮인다.
+- **패널은 절대 앱을 활성화하지 않는다.** `orderFrontRegardless()`만 쓰고 `makeKey…`·`NSApp.activate` 계열은 없다. 활성화되면 `FrontmostAppGate`의 최전면 캐시가 자기 자신으로 덮인다. 화면을 덮는 테두리 패널도 `ignoresMouseEvents`라 클릭을 먹지 않는다.
- **자기 자신(VimAction) pid는 앵커로 삼지 않는다.** 메뉴바 클릭·설정 창이 우리를 최전면으로 만들면 리졸버가 우리 pid에 붙는데, 사다리는 비자신 캐시 축이라 여전히 `.mode`다 — 그대로 두면 배지가 우리 설정 창에 붙는다. 감추지는 않는다(메뉴를 열 때마다 깜빡이면 더 나쁘다).
- **pid 출처는 리졸버**(`EventTapController.observedProcessID`)다 — 디스패치 경로와 같은 앱을 겨눈다. `FrontmostAppGate`가 아니다.
- **좌표 변환은 전역 flip 하나**: `y' = NSScreen.screens[0].frame.maxY - (y + h)`. 보조 디스플레이(AX x 음수)에서도 그대로다.
-- **클램프는 `visibleFrame`, 화면 선택은 `frame`.** 텍스트 뷰가 창 전체인 앱을 최대화하면 "요소 위쪽 바깥" 규칙이 알약을 메뉴바·노치 뒤로 보낸다. 반대로 화면 선택까지 `visibleFrame`으로 하면 메뉴바 띠 안의 앵커가 자기 디스플레이를 못 찾고 주 화면으로 튄다.
+- **클램프는 `visibleFrame`, 화면 선택은 `frame`.** 텍스트 뷰가 창 전체인 앱을 최대화하면 "요소 위쪽 바깥" 규칙이 알약을 메뉴바·노치 뒤로 보낸다. 반대로 화면 선택까지 `visibleFrame`으로 하면 메뉴바 띠 안의 앵커가 자기 디스플레이를 못 찾고 주 화면으로 튄다. 화면 테두리도 같은 축이다 — 앵커 rect(요소 또는 창)와 `frame`이 겹치는 화면을 고르고(AX를 더 읽지 않는다 — 요소는 창 안에 있어 같은 디스플레이다, 없으면 첫 화면), 그 화면의 **`visibleFrame`**을 두른다(`frame`을 두르면 위 변이 메뉴바 뒤, 아래 변이 Dock 뒤에 숨는다). 창 테두리는 이 축을 타지 않는다 — 창 rect를 그대로 두르고(화면 밖으로 나간 만큼 잘린다), 라벨만 앵커 화면의 `visibleFrame`과 창 frame 안으로 민다.
- **앵커가 없으면 재시도하지 않는다** — 그것이 답이고, 다음 앵커 이벤트가 다시 부른다.
-- **글씨 색은 강조색에서 파생된다** — 흰 글씨 대비가 3:1 아래로 떨어지는 밝은 강조색(노랑 1.41:1·초록·주황·그래파이트)에서만 검은 글씨다. 색만으로 구분하지 않는다는 PRD NFR 때문에 라벨 텍스트는 항상 동반된다.
-- **순수 계층은 테스트로 고정된다**: `ModeIndicatorLayoutTests`(사다리·퇴화 rect·배치·flip·보조 디스플레이·클램프·메뉴바 띠·nil), `ModeIndicatorControllerTests`(표시 판정 표·읽기 코얼레싱 판정 표·토글 영속·대비).
-- 테스트에서는 아무 일도 하지 않는다 — 배선이 `bootstrap()`의 XCTest 가드 뒤이고, 입력이 밀린 적 없으면 판정이 통째로 막히며, 패널은 첫 표시에서야 만들어진다.
+- **글씨 색은 강조색에서 파생된다** — 흰 글씨 대비가 3:1 아래로 떨어지는 밝은 강조색(노랑 1.41:1·초록·주황·그래파이트)에서만 검은 글씨다. 색만으로 구분하지 않는다는 PRD NFR 때문에 라벨 텍스트는 항상 동반된다 — 화면 테두리도 모서리 라벨이 같은 알약 뷰라 규칙이 하나다.
+- **flash가 배지 위에 그려지는 것은 창 층의 차이로 보장된다**(flash `.statusBar`, 상시 표시 `.statusBar - 1`). 캐럿이 없는 앱에서는 flash가 배지를 완전히 덮고, 캐럿이 있는 앱에서는 flash가 캐럿 아래·배지가 요소 모서리라 **전환 직후 1초간 같은 라벨 둘이 함께 보인다** — flash는 캐럿부터라는 결정의 직접 결과다.
+- **순수 계층은 테스트로 고정된다**: `ModeIndicatorLayoutTests`(알약별 사다리·퇴화 rect·캐럿 유효성 4종·배치·flip·보조 디스플레이·클램프·메뉴바 띠·nil·화면 테두리 5종·창 테두리 6종), `ModeIndicatorControllerTests`(표시 판정 표(상시 표시 off 행 포함)·읽기 코얼레싱 판정 표(스타일 변경·상시 표시 토글 행 포함)·토글 영속·상시 표시 영속·스타일 영속(raw 값 리터럴 포함)·자기 pid 토큰 무효화·대비).
+- 테스트에서는 아무 일도 하지 않는다 — 배선이 `bootstrap()`의 XCTest 가드 뒤이고, 입력이 밀린 적 없으면 판정이 통째로 막히며, 패널은 첫 표시에서야 만들어진다. 자기 pid 경로는 그 전제 위에서 테스트가 직접 부른다(읽지도 그리지도 않아 `token`만 관측된다 — 그래서 `token`은 `private(set)`).
### 알려진 한계
-- **킬스위치 발동 중에는 배지가 남는다.** 발동 상황은 메인이 굳은 상황이라 `orderOut`이 불릴 수 없다 — 그동안 배지는 "NORMAL"을 계속 주장한다. 1초짜리 flash에는 없던 노출이다(스스로 사라졌다).
-- **Secure Input 진입은 워치독 폴링으로만 관측**되므로 배지가 한 틱만큼 늦게 사라진다 (메뉴바와 같은 기존 지연, 화면에서 더 잘 보일 뿐).
-- **창 드래그 추종은 알림이 오는 만큼이다.** 실측에서 창 기하 변경은 알림이 한 번이 아니라 여러 건 몰려 오고, 1-in-flight 코얼레싱이 그 버스트를 접는다.
+- **킬스위치 발동 중에는 상시 표시가 남는다.** 발동 상황은 메인이 굳은 상황이라 `orderOut`이 불릴 수 없다 — 그동안 배지(또는 테두리)는 "NORMAL"을 계속 주장한다. 1초짜리 flash에는 없던 노출이다(스스로 사라졌다).
+- **Secure Input 진입은 워치독 폴링으로만 관측**되므로 상시 표시가 한 틱만큼 늦게 사라진다 (메뉴바와 같은 기존 지연, 화면에서 더 잘 보일 뿐).
+- **창 드래그 추종은 알림이 오는 만큼이다.** 실측에서 창 기하 변경은 알림이 한 번이 아니라 여러 건 몰려 오고, 1-in-flight 코얼레싱이 그 버스트를 접는다. 배지는 작아서 티가 안 났지만 **창 테두리에서는 이 지연이 눈에 보인다** — 창 크기 테두리가 창을 뒤따라온다.
+- **두 디스플레이에 걸친 창의 라벨은 `NSScreen.screens` 순서로 화면을 고른다** — 최대 교집합이 아니다.
+- **Visual 선택의 캐럿은 선택 시작(`loc`)이다** — `AXSelectedTextRange`의 위치만 쓰므로 선택을 뒤로 늘린 상태의 flash는 선택 시작 아래에 뜬다.
+- **전체화면 앱 위의 테두리는 위 변이 메뉴바 띠만큼 내려온다** — `visibleFrame`은 메뉴바가 자동 숨김이어도 그 띠를 뺀다.
## 근거 요약
-메뉴바 글리프는 시야 밖이라 모드 인지 문제를 풀지 못한다. 표시 정책(전환 시 순간 표시 + 비-Insert 상시 배지), 앵커 사다리와 이벤트 기반 갱신, Chromium 스크린리더 모드 미강제, 설정 소유권은 각각 결정 문서에 있다.
+메뉴바 글리프는 시야 밖이라 모드 인지 문제를 풀지 못한다. 표시 정책(전환 시 순간 표시 + 비-Insert 상시 표시, 창·화면 테두리는 대체 스타일, 순간 표시만은 opt-out), 앵커 사다리(flash는 캐럿부터·상시 표시는 요소부터)와 이벤트 기반 갱신, Chromium 스크린리더 모드 미강제, 설정 소유권은 각각 결정 문서에 있다.
-- 관련 결정: [20260906_mode-indicator-hybrid-display-policy.md](../../decisions/references/20260906_mode-indicator-hybrid-display-policy.md), [20260906_mode-indicator-anchor-ladder-event-driven.md](../../decisions/references/20260906_mode-indicator-anchor-ladder-event-driven.md), [20260906_no-forced-chromium-screen-reader-mode.md](../../decisions/references/20260906_no-forced-chromium-screen-reader-mode.md), [20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md), [20260725_callback-light-invariant.md](../../decisions/references/20260725_callback-light-invariant.md), [20260725_tap-main-runloop-retention.md](../../decisions/references/20260725_tap-main-runloop-retention.md)
+- 관련 결정: [20260906_mode-indicator-hybrid-display-policy.md](../../decisions/references/20260906_mode-indicator-hybrid-display-policy.md), [20260914_mode-indicator-optin-flash-only-and-window-border.md](../../decisions/references/20260914_mode-indicator-optin-flash-only-and-window-border.md), [20260906_mode-indicator-anchor-ladder-event-driven.md](../../decisions/references/20260906_mode-indicator-anchor-ladder-event-driven.md), [20260906_no-forced-chromium-screen-reader-mode.md](../../decisions/references/20260906_no-forced-chromium-screen-reader-mode.md), [20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md), [20260725_callback-light-invariant.md](../../decisions/references/20260725_callback-light-invariant.md), [20260725_tap-main-runloop-retention.md](../../decisions/references/20260725_tap-main-runloop-retention.md)
## 관련
-- 코드: `VimAction/ModeIndicatorController.swift`, `VimAction/ModeIndicatorGeometryReader.swift`, `VimAction/ModeIndicatorLayout.swift`, `VimAction/ModeIndicatorPanel.swift`, `VimAction/FocusedElementResolver.swift`(`onFocusGeometryChanged`·창 알림), `VimAction/EventTapController.swift`(`updateMode`·`onModeChange`·`onFocusGeometryChanged`·`observedProcessID`), `VimAction/AppState.swift`(배선·`indicatorInputs`·`trackIndicatorLadder`·`Mode.overlayLabel`·`Mode.showsPersistentBadge`), `VimAction/SettingsView.swift`(토글)
+- 코드: `VimAction/ModeIndicatorController.swift`(`ModeIndicatorPresentationStyle` 포함), `VimAction/ModeIndicatorGeometryReader.swift`, `VimAction/ModeIndicatorLayout.swift`, `VimAction/ModeIndicatorPanel.swift`(`makeOverlayPanel`·`ModeIndicatorPillView`), `VimAction/ModeIndicatorBorderPanel.swift`, `VimAction/FocusedElementResolver.swift`(`onFocusGeometryChanged`·창 알림), `VimAction/EventTapController.swift`(`updateMode`·`onModeChange`·`onFocusGeometryChanged`·`observedProcessID`), `VimAction/AppState.swift`(배선·`indicatorInputs`·`trackIndicatorLadder`·`Mode.overlayLabel`·`Mode.showsPersistentBadge`), `VimAction/SettingsView.swift`(토글·"Show" Picker·스타일 Picker), `VimAction/Preferences.swift`(키 셋)
- 표시 사다리: [reentrancy-and-safety.md](reentrancy-and-safety.md)(메뉴바 글리프 우선순위) — 오버레이는 그 사다리의 두 번째 소비자다
- pid 출처·읽기 큐 규율·리졸버 알림: [focus-and-dispatch-reads.md](focus-and-dispatch-reads.md)
-- 앱 셸·설정 토글: [app-shell.md](app-shell.md), [profiles-and-config.md](profiles-and-config.md)
+- 앱 셸·설정 토글·Picker: [app-shell.md](app-shell.md), [profiles-and-config.md](profiles-and-config.md)
diff --git a/.claude/skills/architecture/references/profiles-and-config.md b/.claude/skills/architecture/references/profiles-and-config.md
index 33f34be..7282281 100644
--- a/.claude/skills/architecture/references/profiles-and-config.md
+++ b/.claude/skills/architecture/references/profiles-and-config.md
@@ -1,6 +1,6 @@
# 프로파일과 설정
-- **Last updated**: 2026-08-14 (M5 D2 — 번들 기본 전략 `keyboard` → `auto` 전환 완료, 도그푸딩 게이트 통과 뒤 별도 커밋)
+- **Last updated**: 2026-09-06 (온스크린 인디케이터 스타일 키 추가)
## 현재 구조
@@ -96,7 +96,7 @@ actions: # 명령 계열: 그 액션 자신의 키 교
**"사용자가 파일로 관리하고 싶은 설정"은 YAML, "앱이 스스로 쓰는 상태"는 UserDefaults.**
-- UserDefaults 잔류: `interceptionEnabled`(마스터 토글 — 킬스위치가 전용 스레드에서 직접 영속하는 안전 경로라 파일 IO에 의존시키지 않는다), `normalModeEscapeEnabled`(기존 Settings UI 토글 유지), `onScreenModeIndicatorEnabled`(온스크린 모드 인디케이터 on/off, 기본 on — 표시 취향이지 앱별 동작 정의가 아니고, YAML로 두면 Settings UI가 YAML을 써야 한다. 소유자는 `ModeIndicatorController` — [20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md)).
+- UserDefaults 잔류: `interceptionEnabled`(마스터 토글 — 킬스위치가 전용 스레드에서 직접 영속하는 안전 경로라 파일 IO에 의존시키지 않는다), `normalModeEscapeEnabled`(기존 Settings UI 토글 유지), `onScreenModeIndicatorEnabled`(온스크린 모드 인디케이터 on/off, 기본 on)·`onScreenModeIndicatorPersistentEnabled`(상시 표시 on/off — off면 순간 표시만, 기본 on)·`onScreenModeIndicatorStyle`(상시 표시 스타일 — `ModeIndicatorPresentationStyle`의 raw 문자열 `badge`/`windowBorder`/`screenBorder`, 기본 `badge`, 없는 값·모르는 값은 기본) — 셋 다 표시 취향이지 앱별 동작 정의가 아니고, YAML로 두면 Settings UI가 YAML을 써야 한다. 소유자는 `ModeIndicatorController` ([20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md), [20260914_mode-indicator-optin-flash-only-and-window-border.md](../../decisions/references/20260914_mode-indicator-optin-flash-only-and-window-border.md)).
- YAML: 앱별 on/off, 프로파일 전부.
- **어느 쪽도 아닌 셋째 칸이 있다 — 시스템이 소유하는 상태.** 로그인 시 자동 시작은 `SMAppService.mainApp.status`가 SSOT이고 앱은 값을 저장하지 않는다(사용자가 시스템 설정에서 통지 없이 바꾸므로 미러는 반드시 어긋난다 — [app-shell.md](app-shell.md)). Sparkle 자동 확인도 같은 성격으로, 값의 영속은 Sparkle이 소유한다.
diff --git a/.claude/skills/decisions/SKILL.md b/.claude/skills/decisions/SKILL.md
index 67890ad..eb6d020 100644
--- a/.claude/skills/decisions/SKILL.md
+++ b/.claude/skills/decisions/SKILL.md
@@ -67,7 +67,7 @@ description: VimAction 프로젝트의 기술 결정 히스토리 SSOT — 아
| 08-02 | App Icon은 Icon Composer `.icon` | legacy는 macOS 26이 회색 컨테이너에 강제 합성(실측) — appiconset 제거, 소스 벡터는 stroke가 아닌 외곽선 fill이어야 함 (26.5 전제는 20260812 배포 타깃 결정으로 부분 supersede) | [20260802_app-icon-icon-composer.md](references/20260802_app-icon-icon-composer.md) |
| 08-02 | 설정 창 열린 동안만 Dock 아이콘 | `.regular` 승격(앱 메뉴 동반 수용) — 열림=`onAppear`(창이 key가 안 됨, 실측)·닫힘=`willClose` (닫힘 판정 술어는 20260812 창 동일성으로 부분 supersede) | [20260802_dock-icon-while-settings-open.md](references/20260802_dock-icon-while-settings-open.md) |
| 08-09 | UI 쓰기는 라인 편집만 | 재직렬화 금지는 유지, 값 토큰 교체·한 줄 삽입은 허용 — 안전 계약 3종(에러 상태 거부/애매하면 nil+결과 자가검증/파일 열기 폴백) | [20260809_config-yaml-line-edit-writes.md](references/20260809_config-yaml-line-edit-writes.md) |
-| 08-09 | 설정 창은 3탭 | General(권한·Behavior·접힌 Diagnostics)/Apps(기본 선택)/About, 460×560 고정 — 권한 섹션은 신뢰 상태에 따라 자리 이동 | [20260809_settings-window-three-tabs.md](references/20260809_settings-window-three-tabs.md) |
+| 08-09 | 설정 창은 3탭 | General(권한·Behavior·접힌 Diagnostics)/Apps(기본 선택)/About, 460×560 고정 — 권한 섹션은 신뢰 상태에 따라 자리 이동 (탭 수는 20260915 Indicator 탭으로 부분 supersede) | [20260809_settings-window-three-tabs.md](references/20260809_settings-window-three-tabs.md) |
| 08-09 | 미허용 발견성은 push 2종 | 런치 1회 자동 오픈(`openSettings`를 `MenuBarExtra` label에서 — `sendAction`은 macOS 14부터 사망) + 메뉴 상시 경고 행 (26.5 전제는 20260812 배포 타깃 결정으로 부분 supersede — 결론 유지) | [20260809_permission-discoverability-push.md](references/20260809_permission-discoverability-push.md) |
| 08-10 | 자동 업데이트는 Sparkle 2.x | appcast + EdDSA·코드서명 이중 검증, 샌드박스 해제라 XPC 불필요 — 자체 구현 기각 | [20260810_sparkle-auto-update.md](references/20260810_sparkle-auto-update.md) |
| 08-10 | 배포 패키징은 DMG | 첫 설치 UX·설치 위치 안정성 — Sparkle·cask 공용, zip 기각 | [20260810_dmg-packaging.md](references/20260810_dmg-packaging.md) |
@@ -102,6 +102,8 @@ description: VimAction 프로젝트의 기술 결정 히스토리 SSOT — 아
| 09-06 | 앵커 사다리 — 캐럿→요소→창, 이벤트 기반 갱신 | 순간 표시는 캐럿부터·상시 배지는 요소부터, 키마다 재배치·폴링 없음(20260725 재검토 트리거 회피), AX는 메인 밖 50ms·pid만, 캐럿은 `(loc,1)`→`(loc-1,1)`→텍스트 마커 — 앱 계열별 실측표 포함 | [20260906_mode-indicator-anchor-ladder-event-driven.md](references/20260906_mode-indicator-anchor-ladder-event-driven.md) |
| 09-06 | Chromium 스크린리더 모드 강제 안 함 | `AXEnhancedUserInterface`를 켜면 Chrome·Arc 캐럿이 나옴을 실측했으나 브라우저 전역 완전 모드 비용이 비례하지 않음 — 캐럿 없음 수용·요소 폴백, Electron `AXManualAccessibility` 기상은 유지 | [20260906_no-forced-chromium-screen-reader-mode.md](references/20260906_no-forced-chromium-screen-reader-mode.md) |
| 09-06 | 인디케이터 설정은 UserDefaults | on/off·스타일은 Settings General 토글(20260801 경계 기준), config.yaml 비노출, 기본 on — PRD 초안의 YAML 스타일 항목 대체 | [20260906_mode-indicator-settings-in-userdefaults.md](references/20260906_mode-indicator-settings-in-userdefaults.md) |
+| 09-14 | 인디케이터 opt-in 확장 — 순간 표시만 + 창 테두리 | 상시 표시 on/off Bool 키 추가(off면 flash만), 상시 스타일에 포커스 창 테두리(폴백 없음) 추가, 기본값 불변 — 3단 enum 기각(마이그레이션), 자기 pid 경로는 떠 있는 읽기 무효화·flash 만료 | [20260914_mode-indicator-optin-flash-only-and-window-border.md](references/20260914_mode-indicator-optin-flash-only-and-window-border.md) |
+| 09-15 | 설정 창에 Indicator 탭 | General / Indicator / Apps / About 4탭 — 인디케이터 항목 셋을 General > Behavior에서 옮기기만(키·문구 불변), "Visual" 이름·About 앞 배치 기각, 08-09 3탭 결정은 탭 수만 대체 | [20260915_settings-window-indicator-tab.md](references/20260915_settings-window-indicator-tab.md) |
### 이벤트 탭 — 진입·번역·수명
diff --git a/.claude/skills/decisions/references/20260809_settings-window-three-tabs.md b/.claude/skills/decisions/references/20260809_settings-window-three-tabs.md
index 0bddc27..79d74ac 100644
--- a/.claude/skills/decisions/references/20260809_settings-window-three-tabs.md
+++ b/.claude/skills/decisions/references/20260809_settings-window-three-tabs.md
@@ -1,3 +1,5 @@
+> Superseded (부분) by [20260915_settings-window-indicator-tab.md](20260915_settings-window-indicator-tab.md) — 탭 수(3탭 → General / Indicator / Apps / About 4탭) / 권한 섹션 자리 이동·Diagnostics 접기·460×560 고정·기본 선택 Apps는 유효
+
# 설정 창은 3탭 — General / Apps / About
- **결정일**: 2026-08-09
diff --git a/.claude/skills/decisions/references/20260914_mode-indicator-optin-flash-only-and-window-border.md b/.claude/skills/decisions/references/20260914_mode-indicator-optin-flash-only-and-window-border.md
new file mode 100644
index 0000000..1e00066
--- /dev/null
+++ b/.claude/skills/decisions/references/20260914_mode-indicator-optin-flash-only-and-window-border.md
@@ -0,0 +1,38 @@
+# 온스크린 인디케이터 opt-in 확장 — 순간 표시만 옵션 + 포커스 창 테두리 스타일
+
+
+
+- **결정일**: 2026-09-14
+
+## 결정
+
+온스크린 모드 인디케이터에 **opt-in 옵션 둘**을 추가한다. 기본값은 바꾸지 않는다 (전환 시 순간 표시 + 비-Insert 상시 배지).
+
+1. **상시 표시 on/off** (`onScreenModeIndicatorPersistentEnabled`, 기본 on): off면 모드 전환 때의 순간 표시(flash)만 남고 Normal·Visual 동안 아무것도 남지 않는다. 기존 on/off 토글은 그대로 두고 그 아래 Picker("Only when the mode changes" / "Also while in Normal or Visual mode")로 고른다 — Bool 키 하나를 더하는 additive 방식이라 저장값 마이그레이션이 없다.
+2. **상시 표시 스타일에 포커스 창 테두리 추가** (`ModeIndicatorPresentationStyle.windowBorder`): 포커스 창 rect 자체를 강조색 테두리로 두르고 창 안쪽 오른쪽 위에 모드 라벨을 놓는다. 배지 / 창 테두리 / 화면 테두리 셋 중 고른다. 창이 없으면 **폴백 없이** 아무것도 표시하지 않는다.
+
+같이 확정한 것: 자기 pid 경로(설정 창·메뉴바 클릭으로 VimAction이 최전면)는 떠 있는 기하 읽기를 **무효화**하고 밀린 flash를 **만료**한다. 두 옵션 모두 설정 창에서 바꾸므로 이 경로의 정합성이 전제다 (PR #68 리뷰에서 지적된 구멍).
+
+## 배경·근거 (왜)
+
+PR 3 도그푸딩 피드백. [20260906_mode-indicator-hybrid-display-policy.md](20260906_mode-indicator-hybrid-display-policy.md)가 "순간 표시만"과 "창 단위 하이라이트"를 **기본으로는** 기각한 이유는 유지된다 — 순간 표시만으로는 사후 인지("딴 데 갔다 와서 지금 Normal이었나?")를 못 풀고, 창 단위 표시는 어느 입력칸인지 말하지 못한다. 그래서 기본값은 그대로다. 다만:
+
+- 상시 배지가 **소음**으로 느껴지는 사용자에게는 순간 표시만이 더 낫다. 안전 장치를 통째로 끄는 것(토글 off)과 전환 피드백은 남기는 것 사이에 단계가 없었다.
+- 창 테두리는 라벨을 동반하므로 "어느 모드인지"는 말한다(색만으로 구분하지 않는다는 NFR 유지). "어느 입력칸인지"를 포기하는 대신 시야 어디서든 보이는 큰 신호를 원하는 사용자의 선택지다 — 화면 테두리보다 덜 압도적이고 배지보다 눈에 띈다.
+- 두 옵션은 기존 구조에 거의 공짜다: 순간 표시만은 Insert 경로(`showsBadge == false`)와 같은 코드이고, 창 테두리는 이미 있는 테두리 패널과 창 rect 읽기·창 이동/리사이즈 알림을 재사용한다.
+- 자기 pid 토큰: 두 새 설정도 기존 스타일 설정과 같이 설정 창(= 자기 pid 경로)에서 바뀐다. 이 경로가 떠 있는 읽기를 살려 두면 이전 앱을 향해 떠 있던 읽기가 착지해 방금 감춘 옛 형태를 되살린다. 밀린 flash를 만료시키는 것은 "앵커를 못 찾은 flash는 버린다"는 기존 계약과 같은 원칙이다.
+
+## 검토한 대안
+
+- **on/off 토글을 3단 Picker(off / 전환 시만 / 항시)로 교체**: 기각 — 기존 Bool 키를 enum으로 바꾸려면 마이그레이션 코드와 테스트가 따라온다. 토글 + 하위 Picker면 UX가 같고 코드가 절반이다.
+- **창 테두리를 창 사다리 폴백으로만 흡수**(09-06 결정의 원안): 유지 — 폴백은 그대로 있고, 이번 것은 사용자가 **고르는** 스타일이다. 둘은 다른 축이다.
+- **창 테두리에서 창이 없을 때 요소·화면으로 폴백**: 기각 — "창 테두리"인데 다른 것을 두르면 사용자가 고른 스타일이 아니다. 배지가 앵커 없을 때와 같은 답("없음")이 일관된다.
+- **테두리 모서리 반경을 스타일별로 가르기**: 보류 — 선 굵기의 2배(8pt)를 두 테두리가 공유한다. 실기기에서 창 모서리와 어긋나 보이지 않아 갈라야 할 이유가 아직 없다.
+- **자기 pid 경로에서 밀린 flash 보존**: 기각 — 보존하면 복귀 시점의 앵커 이벤트에 얹혀 한참 전 전환의 라벨이 뒤늦게 번쩍인다.
+
+## 영향 범위
+
+- 갱신한 architecture reference: [mode-indicator-overlay.md](../../architecture/references/mode-indicator-overlay.md), [profiles-and-config.md](../../architecture/references/profiles-and-config.md)(UserDefaults 키 셋), [app-shell.md](../../architecture/references/app-shell.md)(Behavior 항목)
+- 코드: `ModeIndicatorController`(설정 프로퍼티 셋, `borderPanelStyle`, 자기 pid 토큰 무효화, `usesBorderPanel`), `ModeIndicatorGeometryReader.read(processID:includesCaret:includesWindow:)`, `ModeIndicatorLayout.windowBorderLayout`/`screenBorderLayout`(개명), `Preferences.swift`, `SettingsView.swift`.
+- Settings General > Behavior의 인디케이터 항목이 셋(토글·"Show" Picker·"Indicator style" Picker)이 됐다. 색상·투명도 후속에서 Visual 탭 분리를 검토할 때 이 셋도 같이 옮긴다 (탭 구성은 [20260809_settings-window-three-tabs.md](20260809_settings-window-three-tabs.md)).
+- 09-06 결정 다섯 항목을 뒤집지 않는다 — Supersedes 없음.
diff --git a/.claude/skills/decisions/references/20260915_settings-window-indicator-tab.md b/.claude/skills/decisions/references/20260915_settings-window-indicator-tab.md
new file mode 100644
index 0000000..a5ce4e0
--- /dev/null
+++ b/.claude/skills/decisions/references/20260915_settings-window-indicator-tab.md
@@ -0,0 +1,34 @@
+# 설정 창에 Indicator 탭 — General / Indicator / Apps / About
+
+
+
+- **결정일**: 2026-09-15
+
+## 결정
+
+설정 창을 4탭으로 확장한다: **General / Indicator / Apps / About**. 온스크린 모드 인디케이터 항목 셋(on/off 토글 · "Show" Picker · "Indicator style" Picker와 각주 셋)을 General > Behavior에서 **Indicator** 탭의 단일 그룹 `Form` 섹션으로 **옮기기만** 한다 — 새 컨트롤·문구·설정 키 없음, 순서와 `.disabled` 조건 불변. General의 Behavior에는 "Launch at login"과 "Exit Normal mode on ⌘/⌥ shortcuts"만 남는다. 탭 아이콘은 SF Symbol `text.cursor`.
+
+[20260809_settings-window-three-tabs.md](20260809_settings-window-three-tabs.md)의 **탭 수(3탭)만** 이 결정이 대체한다. 나머지 규칙 — 권한 섹션의 신뢰 상태별 자리 이동, Diagnostics 접기, 460×560 고정, 기본 선택 Apps(온보딩 첫 창만 General) — 은 그대로 유효하다.
+
+## 배경·근거 (왜)
+
+- **인디케이터 항목이 Behavior 섹션을 밀어냈다.** PR 3 확장([20260914_mode-indicator-optin-flash-only-and-window-border.md](20260914_mode-indicator-optin-flash-only-and-window-border.md))으로 토글 하나가 컨트롤 셋 + 각주 셋이 되어 섹션의 대부분을 차지했다.
+- **성격이 다른 것이 한 섹션에 섞였다.** 로그인 시 자동 시작·Normal 탈출은 시스템 동작이고, 인디케이터는 화면 표시 취향이다.
+- **색상·투명도 후속에서 항목이 더 는다.** 09-14 결정은 그 후속에서 탭 분리를 검토하기로 했지만, 자리를 먼저 마련해 두면 후속은 탭에 항목만 더하면 된다.
+
+## 검토한 대안
+
+- **General에 유지**: 위 두 문제(밀어냄·혼재)가 그대로 남고, 후속 항목이 붙으면 더 나빠진다.
+- **색상·투명도 후속까지 대기**(09-14 결정의 원래 시점): 지금 이미 섹션이 과밀하다. 분리 비용이 뷰 이동뿐이라 미룰 이유가 없다.
+- **탭 이름 "Visual"**(플랜 가칭): 기능명이 아니다 — Vim의 Visual 모드와도 헷갈린다. 탭에 담기는 것은 인디케이터 기능이다.
+- **About 바로 앞 배치**(General / Apps / Indicator / About): 인디케이터는 설정성 항목이라 General 곁이 자연스럽고, 참조용 탭(Apps 상태·About)은 뒤에 모인다.
+
+## 영향 범위
+
+- 갱신한 architecture reference: [app-shell.md](../../architecture/references/app-shell.md)(설정 창 탭 구성), [mode-indicator-overlay.md](../../architecture/references/mode-indicator-overlay.md)(설정 위치)
+- 코드: `VimAction/SettingsView.swift` — `SettingsTab.indicator`, `IndicatorTab` 추가, `GeneralTab`에서 인디케이터 항목·바인딩 제거. `ModeIndicatorController`·`Preferences.swift`·테스트 무변경(값·영속 소유는 컨트롤러, 뷰는 바인딩만).
+- Dock 아이콘 훅(`SettingsWindowReader`·`onAppear`)은 계속 `TabView` 루트에 있다.
+
+## Supersedes
+
+- [20260809_settings-window-three-tabs.md](20260809_settings-window-three-tabs.md) — 부분: 탭 수(3탭 → 4탭)만. 나머지 규칙은 유효해 인덱스에 남긴다.
diff --git a/.claude/skills/plans/SKILL.md b/.claude/skills/plans/SKILL.md
index 6d2440e..dbf3225 100644
--- a/.claude/skills/plans/SKILL.md
+++ b/.claude/skills/plans/SKILL.md
@@ -51,4 +51,4 @@ description: VimAction 프로젝트의 진행 중 작업 플랜(단기 기억) S
|---|---|---|---|---|
| 2026-08-02 | 2026-08-18 | M5 — AX 어댑터 + auto 전략 | MVP 1단계(M1~M4) 완료 후의 1차 확장. AX 읽기 혼용이 축 — M3 수용 엣지 대부분이 여기서 해소된다. **PR 단위 7개로 분해됨**(A 읽기 기반 → B·C 혼용 → D AX 어댑터·auto → E 스키마), PR마다 worktree. **PR-A·B·C1·C2 + D1-설계 세션 + PR-D1a(#39) + PR-D1b([PR #41](https://github.com/pilyang/vim-action/pull/41), main `544005c`) 머지 완료 — D1 트랙 종료**(AX 쓰기 통로·단일 `.ax` 드라이버·`FocusedTextOffsets`·편집/삽입/Visual 하이브리드·`strategy` 파싱). **PR-D2 머지 완료**([#56](https://github.com/pilyang/vim-action/pull/56) main `d8d155c` + 문서 [#57](https://github.com/pilyang/vim-action/pull/57) main `1c62cb7`, 2026-08-18 — auto 프로브·거짓말 감지·force-text·기본값 auto + 세션 6 브라우저 클래스 규칙·숨은 입력 기하 신호). **다음은 PR-E**(스키마 확장·마감 — `per_element`, D2 이월: 브라우저 per-element 판정·value-슬라이스 폴백·킬스위치 #59) | [20260802_m5-ax-adapter.md](references/20260802_m5-ax-adapter.md) |
| 2026-08-18 | 2026-08-18 | Normal mode exit 키 허용목록 | **미착수 — 예정 기록용.** `Exit Normal mode on ⌘/⌥ shortcuts`를 전체 on/off에서 "기본은 전환 안 함 + 등록한 키 조합에서만 Normal → Insert"로 고도화. 등록은 키를 직접 눌러 캡처하는 UI | [20260818_normal-mode-exit-key-allowlist.md](references/20260818_normal-mode-exit-key-allowlist.md) |
-| 2026-09-06 | 2026-09-06 | 온스크린 모드 인디케이터 (HUD) | PRD §7.7·로드맵 Stage 4. 조사·보고 + 실기기 스파이크 완료(AX 기하 가용성 실측표, 비활성화 NSPanel 오버레이 검증). 권장안 채택 + decisions 4건 기록 + PR 1([#66](https://github.com/pilyang/vim-action/pull/66)) 머지 + **PR 2 구현·도그푸딩 완료([#67](https://github.com/pilyang/vim-action/pull/67) 머지 대기 — 상시 배지·재앵커·Settings 토글)**. **다음은 PR 3**(캐럿 정밀도 + 화면 테두리 스타일, Fable 워커) | [20260906_onscreen-mode-indicator.md](references/20260906_onscreen-mode-indicator.md) |
+| 2026-09-06 | 2026-09-15 | 온스크린 모드 인디케이터 (HUD) | PRD §7.7·로드맵 Stage 4. 조사·보고 + 실기기 스파이크 완료(AX 기하 가용성 실측표, 비활성화 NSPanel 오버레이 검증). 권장안 채택 + decisions 5건 기록 + PR 1([#66](https://github.com/pilyang/vim-action/pull/66)) 머지 + PR 2([#67](https://github.com/pilyang/vim-action/pull/67)) 머지 — 상시 배지·재앵커·Settings 토글. **PR 3(캐럿 아래 flash + 화면 테두리) + 확장(순간 표시만 옵션·창 테두리 스타일·자기 pid 토큰 수정) 구현·스크립트 도그푸딩 완료 + Settings 창 Indicator 탭 분리, [PR #68](https://github.com/pilyang/vim-action/pull/68) 오픈** — 다음은 사용자 직접 도그푸딩(Settings UI 경로·Indicator 탭·창 테두리 엣지) → 머지 확인, 그 뒤 외양 커스텀 후속(색상·투명도 — Indicator 탭에 추가) | [20260906_onscreen-mode-indicator.md](references/20260906_onscreen-mode-indicator.md) |
diff --git a/.claude/skills/plans/references/20260906_onscreen-mode-indicator.md b/.claude/skills/plans/references/20260906_onscreen-mode-indicator.md
index 467a376..eadb281 100644
--- a/.claude/skills/plans/references/20260906_onscreen-mode-indicator.md
+++ b/.claude/skills/plans/references/20260906_onscreen-mode-indicator.md
@@ -1,7 +1,7 @@
# 온스크린 모드 인디케이터 (HUD)
- **생성일**: 2026-09-06
-- **갱신일**: 2026-09-06 (PR 2 구현·도그푸딩 완료, PR #67 오픈, 외양 커스텀 후속 항목 추가)
+- **갱신일**: 2026-09-15 (Settings 창 Indicator 탭 분리를 PR #68에 추가 — 이전: PR 3 확장 커밋·스크립트 도그푸딩 완료, Settings UI 경로·창 테두리 엣지는 사용자 직접 도그푸딩으로 인계)
## 목표
@@ -12,16 +12,19 @@
- [x] 세 가지 표시안(캐럿 근처 / 포커스 요소 근처 / 화면 테두리)과 표시 정책(항시 vs 전환 시) 조사·보고 — 세 안은 배타적이 아니라 **앵커 정밀도 사다리**(캐럿 → 요소 → 창·화면 → 메뉴바)로 묶는 설계를 권장. 권장 조합: 모든 모드 전환 시 캐럿 근처 순간 표시(~1초 페이드) + Normal·Visual 동안 요소 모서리 상시 배지 + Insert는 무표시. 화면 테두리는 선택 스타일.
- [x] 구현 가능성 스파이크 (2026-09-06, 실기기·2디스플레이): AX 기하 가용성 실측 + 비활성화 NSPanel 오버레이 프로토타입 검증. 결과는 아래 "진행 중 컨텍스트".
- [x] **PR 1 머지 완료** ([PR #66](https://github.com/pilyang/vim-action/pull/66) → main `48cbd28`, 2026-09-06; Opus 워커 위임 + 감독 리뷰 + 독립 검증): 비활성화 패널·순수 레이아웃(테스트 10건)·전용 큐 기하 리더·모드 전환 훅. 실기기 도그푸딩(Developer ID Release 설치): TextEdit Esc/i/v, Chrome Esc, Slack Esc 전부 라벨 표시·최전면 불변·보조 디스플레이 정렬 확인. architecture reference [mode-indicator-overlay.md](../../architecture/references/mode-indicator-overlay.md) 추가.
-- [x] **PR 2 구현 완료** ([PR #67](https://github.com/pilyang/vim-action/pull/67), 브랜치 `feat/mode-indicator-persistent-badge`, Opus 워커 위임 + 감독 리뷰 + 독립 검증): 비-Insert 상시 배지(flash보다 한 단 작고 한 층 아래)·재앵커 트리거 5종(모드 전환 / 포커스·앱 활성화·창 이동·리사이즈 / 디스플레이 재구성 / 사다리 관찰 루프 / 토글)이 단일 reconcile로 합류, 순수 판정 2종(`presentation`·`needsGeometryRead`) 표 테스트, `visibleFrame` 클램프(화면 선택은 `frame`), 강조색 luminance 기반 글씨색, General > Behavior 토글(`onScreenModeIndicatorEnabled`, 기본 on). 실기기 도그푸딩: 창 이동(−900,+262)·리사이즈 추종, Chrome 전환 재앵커, disabled 앱(Terminal)에서 숨김, 복귀 시 재표시, Insert 무배지, VISUAL 배지 — 전부 확인. architecture 4종 갱신(profiles-and-config의 키 반영 포함).
+- [x] **PR 2 머지 완료** ([PR #67](https://github.com/pilyang/vim-action/pull/67) → main `4040fe4`, 2026-09-06, 브랜치 `feat/mode-indicator-persistent-badge`, Opus 워커 위임 + 감독 리뷰 + 독립 검증): 비-Insert 상시 배지(flash보다 한 단 작고 한 층 아래)·재앵커 트리거 5종(모드 전환 / 포커스·앱 활성화·창 이동·리사이즈 / 디스플레이 재구성 / 사다리 관찰 루프 / 토글)이 단일 reconcile로 합류, 순수 판정 2종(`presentation`·`needsGeometryRead`) 표 테스트, `visibleFrame` 클램프(화면 선택은 `frame`), 강조색 luminance 기반 글씨색, General > Behavior 토글(`onScreenModeIndicatorEnabled`, 기본 on). 실기기 도그푸딩: 창 이동(−900,+262)·리사이즈 추종, Chrome 전환 재앵커, disabled 앱(Terminal)에서 숨김, 복귀 시 재표시, Insert 무배지, VISUAL 배지 — 전부 확인. architecture 4종 갱신(profiles-and-config의 키 반영 포함).
+- [x] **PR 3 구현·도그푸딩 완료** (브랜치 `feat/mode-indicator-caret-and-border`, Fable 워커 위임 + 감독 리뷰 + 독립 검증, [PR #68](https://github.com/pilyang/vim-action/pull/68)): flash 앵커가 캐럿부터(읽기 순서 `(loc,1)` → `(loc-1,1)` → 텍스트 마커, 유효성 규칙은 순수 계층 `isUsableCaret` 한 벌, flash 요청 때만 캐럿 읽기), 상시 표시 스타일 `onScreenModeIndicatorStyle`(배지 / 화면 테두리 — 앵커가 속한 디스플레이의 `visibleFrame` + 오른쪽 위 라벨, General 토글 아래 Picker). 실기기 도그푸딩(Developer ID Release, 2디스플레이): TextEdit·Notes는 `(loc,1)`, Safari 주소창은 `(loc-1,1)`(끝 캐럿이 필드 밖이라 첫 변형 기각), Slack·Notion은 마커 — 전부 flash가 캐럿 바로 아래(±1px); Chrome·Arc는 요소 모서리 폴백; 테두리 스타일은 창이 있는 디스플레이만 두르고 Insert에서 사라짐. architecture 3종 갱신.
+- [x] **PR 3 확장 구현 완료** (2026-09-14~15, 도그푸딩 피드백, 같은 브랜치·PR #68에 커밋 3개 `7b92d9d`·`87e7529`·`2d2b738` + 문서 커밋; 이 세션이 직접 구현): ① 자기 pid 경로 토큰 무효화 + 밀린 flash 만료(Copilot 코멘트, `token`을 `private(set)`으로 열어 회귀 테스트) ② 상시 표시 on/off `onScreenModeIndicatorPersistentEnabled`(off면 flash만 — Insert 경로 재사용, "Show" Picker, 스타일 Picker는 상시 표시 없으면 비활성) ③ 상시 스타일 `windowBorder`(포커스 창 rect 테두리 + 창 안쪽 오른쪽 위 라벨, 창 rect는 `includesWindow`로 그 스타일에서만 읽음, 테두리 패널 공유 + `borderPanelStyle`로 창↔화면 전환도 즉시 숨김, 라벨은 화면·창 이중 클램프, `borderLayout` → `screenBorderLayout` 개명). decisions 1건([20260914_mode-indicator-optin-flash-only-and-window-border.md](../../decisions/references/20260914_mode-indicator-optin-flash-only-and-window-border.md)) + architecture 3종 갱신. 스크립트 도그푸딩(Developer ID Release 설치, 2디스플레이): 기본값 회귀(flash+배지), 창 테두리 bounds == TextEdit 창(이동·리사이즈 추종, Slack 전환, 최소화 시 숨김·복원 시 재표시, 보조 디스플레이 x=−2094 정합, 외양 스크린샷 확인), flash-only 두 스타일(flash만·Slack 전환 시 무표시), Insert 숨김 — 전부 수치 확인.
+- [x] **Settings 창 Indicator 탭 분리** (2026-09-15, PR #68에 포함): 인디케이터 항목 셋을 General > Behavior에서 새 Indicator 탭(General 바로 뒤)으로 옮기기만 — 키·문구·기본 탭 불변. decisions [20260915_settings-window-indicator-tab.md](../../decisions/references/20260915_settings-window-indicator-tab.md)(08-09 3탭 결정 탭 수만 부분 supersede) + architecture app-shell·mode-indicator-overlay 갱신.
- [x] 방향 확정(2026-09-06, 권장안 채택) + decisions 4건 기록: 표시 정책 / 앵커 사다리·이벤트 기반 갱신(실측표 포함) / Chromium 스크린리더 모드 강제 안 함 / 설정은 UserDefaults·기본 on.
## 남은 것
-- [ ] [PR #67](https://github.com/pilyang/vim-action/pull/67) 머지 확인 (CI 3잡 required).
-- [ ] **PR 3 — 캐럿 정밀도 + 화면 테두리 스타일.** 순간 표시 앵커를 캐럿부터 시작: `AXBoundsForRange (loc,1)` → 문서 끝 `(loc-1,1)` → `AXSelectedTextMarkerRange`+`AXBoundsForTextMarkerRange`, 마커 결과가 요소 rect와 같으면 무효. 스타일 선택(배지 / 화면 테두리 — 포커스 창이 있는 `NSScreen` frame 패널 + 모서리 라벨). 검증: 실측표 앱들(TextEdit·Notes·Safari·Slack·Notion은 캐럿 아래, Chrome·Arc는 요소 모서리 폴백).
-- [ ] **후속 — 인디케이터 외양 커스텀(색상·투명도).** 모드 라벨 알약(flash·배지)의 **색상**과 **투명도**를 사용자가 고를 수 있게 한다. 시점은 PR 3(캐럿 위치 지정) **이후**, 또는 작업 크기가 작으면 PR 3에 **함께**. 이러면서 설정 항목이 늘어나면(on/off·스타일·색상·투명도) 설정 창에 별도 **Visual 탭**을 두는 것도 함께 검토 — 탭 구성은 [20260809_settings-window-three-tabs.md](../../decisions/references/20260809_settings-window-three-tabs.md)의 결정이라 바꾸면 decisions로 기록하고 app-shell reference를 갱신한다. 색상 커스텀에서도 라벨 텍스트는 항상 동반(색만으로 구분하지 않음 — PRD NFR)하고, 글씨색은 PR 2의 luminance 파생을 사용자 색에 그대로 적용한다. 설정 소유권은 UserDefaults([20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md)).
+- [ ] **사용자 직접 도그푸딩(2026-09-15 인계, `/Applications`에 PR 3 확장 빌드 설치됨, 설정은 제품 기본값으로 리셋)** — Settings UI 경로: "Show" Picker 전환(상시 표시 즉시 숨김·앱 복귀 시 재표시, 스타일 Picker 비활성), 스타일 Picker 3종 전환(설정 창 열린 채 이전 형태 즉시 숨김·복귀 시 새 형태 — 특히 창↔화면 테두리), 배지 떠 있는 채로 메뉴바 아이콘 반복 클릭(배지 되살아남·깜빡임 없음), Settings 창에서 Esc·i(이전 앱 배지 즉시 숨김·늦은 flash 없음). Indicator 탭 외양·탭 전환(General↔Indicator↔Apps, 창 크기 고정·Picker 비활성 표시). 창 테두리 엣지: 실제 마우스 드래그 추종(지연·잔상·CPU), 전체화면 창, 화면 밖으로 일부 나간 창(라벨 클램프), 작은 창(계산기), 두 디스플레이에 걸친 창, 시트·팝오버 열린 상태, 라벨이 툴바를 덮는 정도, 모서리 반경 외양, Chrome·Arc 창. 관측 1건(재현 안 됨): 디스플레이 간 창 이동 직후 flash가 창 오른쪽 아래(1571,1082)에 한 번 떴음 — 재현되면 보고.
+- [ ] [PR #68](https://github.com/pilyang/vim-action/pull/68) 머지 확인 (CI 3잡 required, Copilot 스레드 resolve됨) — 머지되면 `/Applications`의 도그푸딩 빌드(1.0 (1))를 정식 릴리스로 되돌릴지 결정.
+- [ ] **후속 — 인디케이터 외양 커스텀(색상·투명도).** 모드 라벨 알약(flash·배지)의 **색상**과 **투명도**를 사용자가 고를 수 있게 한다. 시점은 PR 3(캐럿 위치 지정) **이후**, 또는 작업 크기가 작으면 PR 3에 **함께**. 새 항목은 설정 창의 **Indicator 탭**에 더한다([20260915_settings-window-indicator-tab.md](../../decisions/references/20260915_settings-window-indicator-tab.md)). 색상 커스텀에서도 라벨 텍스트는 항상 동반(색만으로 구분하지 않음 — PRD NFR)하고, 글씨색은 PR 2의 luminance 파생을 사용자 색에 그대로 적용한다. 설정 소유권은 UserDefaults([20260906_mode-indicator-settings-in-userdefaults.md](../../decisions/references/20260906_mode-indicator-settings-in-userdefaults.md)).
- [ ] 마무리: 로드맵 Stage 4 항목 체크, 플랜 완료 처리.
- [ ] 후속 검토(별도 PR 후보): **막 실행된 앱의 AXObserver 등록 실패.** 앱을 실행하며 동시에 활성화하면 리졸버의 `AXObserverCreate`/`AddNotification`이 `cannotComplete`(−25204)로 실패하고, 리졸버는 다음 pid 전환까지 재시도하지 않는다(PR 2 도그푸딩에서 TextEdit·Chrome·Terminal 콜드 실행 3건 전부 재현, 다른 앱에 갔다 오면 정상). PR 2 이전부터 있던 동작이지만 배지가 생기며 체감이 커졌다 — 그 앱에서는 앱 전환 전까지 포커스·창 이벤트 재앵커가 없고 모드 전환만 배지를 옮긴다. 후보: 등록 실패 시 짧은 지연 뒤 1회 재시도.
@@ -62,7 +65,7 @@
- 텍스트 뷰가 창 전체인 앱(TextEdit)은 "요소 위쪽 바깥" 규칙으로 배지가 타이틀바 자리에 뜬다 — 글자를 안 가려 수용. Chrome은 페이지 로드 직후 앵커가 웹 영역/창으로 잡혀 창 오른쪽 위에 뜸(폴백 정상).
- 라벨은 강조색 배경 — 글씨색은 PR 2부터 강조색 luminance로 파생(흰 글씨 대비 3:1 미만이면 검정: 노랑·주황·초록·다크 그래파이트). 실측 대비비는 `ModeIndicatorPanel.textColor(on:)` 주석에.
- CLAUDE.md의 Swift 6 프로브(`SWIFT_VERSION=6.0` 오버라이드)가 이제 Yams SPM 체크아웃까지 적용돼 앱 타깃 전에 실패한다(깨끗한 HEAD도 동일). 별도 정리 필요.
-- 도그푸딩 도구(scratchpad): `postkey [shift]`(HID 탭에 키 게시, 마커 없음), `vawin`(VimAction 소유 온스크린 창 나열 → 오버레이 표시 여부 판정), `dogfood.sh`(설치→앱 순환→키 게시→캡처). PR 2용 `dogfood3.sh`: 앱을 먼저 띄워 웜 상태로 만들고(콜드 실행은 AX 옵저버 등록 실패) AeroSpace 창을 `aerospace layout floating`으로 풀어 창 이동을 실제로 일으킨다 — 타일링 상태에서는 `set position`이 무시된다.
+- 도그푸딩 도구(scratchpad): `postkey [shift]`(HID 탭에 키 게시, 마커 없음), `vawin`(VimAction 소유 온스크린 창 나열 → 오버레이 표시 여부 판정), `dogfood.sh`(설치→앱 순환→키 게시→캡처). PR 2용 `dogfood3.sh`: 앱을 먼저 띄워 웜 상태로 만들고(콜드 실행은 AX 옵저버 등록 실패) AeroSpace 창을 `aerospace layout floating`으로 풀어 창 이동을 실제로 일으킨다 — 타일링 상태에서는 `set position`이 무시된다. PR 3용 `dogfood4.sh `: 앱마다 `axprobe fields`로 캐럿을 놓고 Esc 뒤 `vawin`(flash 창 bounds)과 `axprobe sample `(캐럿 rect)을 대조해 "flash.x == caret.minX, flash.y == caret.maxY+4"를 수치로 판정, 이어서 `onScreenModeIndicatorStyle`을 `defaults write`로 바꿔 재실행해 테두리 스타일 검증(스타일은 init에서 읽으므로 실행 중 `defaults write`는 무시). `screens`는 디스플레이 frame/visibleFrame을 CG 좌표로 출력. AeroSpace에서는 floating 창도 자기 워크스페이스 모니터에 묶여 스크립트로 다른 디스플레이에 못 옮긴다 — `aerospace move-node-to-monitor --focus-follows-window `를 쓴다. **PR 3 확장 세션(2026-09-15)에서 도구를 다시 만들었다** — `postkey`·`vawin`(스타일 인자로 소유 앱 지정, CG 좌표·layer·alpha 출력)·`relaunch.sh