diff --git a/applications/SprPoP/docs/BUGS_OPEN.md b/applications/SprPoP/docs/BUGS_OPEN.md
index 95119d2..e9174ca 100644
--- a/applications/SprPoP/docs/BUGS_OPEN.md
+++ b/applications/SprPoP/docs/BUGS_OPEN.md
@@ -21,7 +21,7 @@
| [T-1](#t-1) | пики перерисовываются безусловно | оптимизация | открыт |
| [BUG-SPIKE-1](#bug-spike-1) | пики залипают выдвинутыми рядом с Кидом | **низкий** | маловоспроизводим: ни сценарием, ни попиксельной подгонкой X не поднимается |
| [BUG-CHOMP-JUMP-1](#bug-chomp-jump-1) | прыжок с места вплотную к чомперу: кадр с отступом назад | **низкий** | маловоспроизводим: на повторе не поднялся; на пререлиз |
-| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | открыт: подозрение на клавиатурный модуль, не на физику |
+| [GRAB-KBD-TIMING](#grab-kbd-timing) | зацеп в падении срабатывает менее стабильно, чем в оригинале | **после всех уровней** | подозрение на клавиатуру ПОДТВЕРДИЛОСЬ частично: кража байт DSS устранена 2026-08-28, перепроверить |
| [GUARD-ENTRY-DEATH](#guard-entry-death) | вход в комнату вплотную к стражу = мгновенная смерть (не портирован `bump_into_opponent`) | порт | **фикс есть, ждёт проверки в MAME** |
| [BUG-SHADOW-SET](#bug-shadow-set) | Тень на 12-м уровне дерётся спрайтами СТРАЖА: SHADOW.DAT у нас нет вовсе | порт | открыт, разбор готов |
| [PAL-L1-AFTER-INTRO](#pal-l1-after-intro) | вход в игру на уровень 1 после интро — игровая палитра не установлена (чёрный экран) | палитра/fade | открыт: воспроизведение нестабильно, корень не установлен |
@@ -32,7 +32,7 @@
| [MOB-CLIP-RIGHT](#mob-clip-right) | окна и узор кладки рисуются ПОВЕРХ падающих плит | окклюзия | открыт: корень найден 2026-08-13 |
| [MID-OVERLAY-LAYER](#mid-overlay-layer) | оверлей кромки всегда поверх плит и персонажей (у оригинала — midtable с сортировкой) | слои | открыт: нужен разбор архитектуры |
| [GUARD-FALLOUT-VICTORY](#guard-fallout-victory) | страж, выпавший из комнаты, не засчитывается убитым | порт | открыт: две строки |
-| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | открыт: диагноз полный, чинить не сейчас (решение пользователя 2026-08-24) |
+| [KBD-STUCK-WAIT](#kbd-stuck-wait) | потерянный break-код клавиши вешает игру НАСМЕРТЬ в межуровневой заставке | клавиатура/клин | **корень найден и устранён** 2026-08-28 (байты крал KEYSCAN DSS); ждёт полевой проверки |
| [L10-BUTTON-DEBRIS](#l10-button-debris) | ур.10 к.1: плита упала на кнопку (1,8) — щебень есть, а решётки закрыты | кнопки/ворота | открыт: **не воспроизводится**, проверено многое (см. разбор) |
| [LOOSE-SEAM-ANIM](#loose-seam-anim) | плита в шве (col −1), уроненная в прошлой комнате, исчезает без анимации падения | **мелкий** | открыт по решению пользователя: «пока пусть будет так» |
| [SND-PACE-DEAD](#snd-pace-dead) | пейсинг сцен по насосу CBL не включается: признак «часы идут» вычисляется двумя чтениями подряд | **тайминг/звук** | открыт: диагноз полный, симптом обойдён подгонкой (2026-08-27) |
@@ -98,8 +98,67 @@ POS=18`, MAME): Кид стоит на кнопке -> решётка `(2,7)` о
## KBD-STUCK-WAIT. Потерянный break-код вешает игру насмерть в межуровневой заставке
-> **Решение пользователя (2026-08-24): не чинить сейчас.** Записан разбор и
-> варианты; вернуться, когда дойдут руки до клавиатурного слоя.
+> **КОРЕНЬ НАЙДЕН И УСТРАНЁН 2026-08-28.** Байты у нас крал сам DSS — см.
+> раздел «Корень» ниже. Правка в `libc/irq/_irq_tramp.c`. Запись остаётся
+> открытой до полевой проверки и решения по страховке-таймауту.
+
+## Корень (2026-08-28): скан-коды воровал KEYSCAN DSS
+
+Пришёл из чтения исходников DSS (`docs/sources/Estex-DSS/DSS/`, подсказка
+пользователя). Обработчик прерывания DSS живёт в IM1 по `0x0038`
+(`DSS-MAIN.ASM`), и его `INTx38_Handler` ПЕРВЫМ ДЕЛОМ делает `CALL KEYSCAN`,
+а `KEYSCAN.RESCAN` (`KEYINTER.ASM`) вычерпывает FIFO SIO досуха.
+
+Наш трамплин проверял «есть ли клавиатурный байт» ОДИН раз, на входе в
+прерывание, а хвост кадрового пути уходил в DSS (`jp 0x0038`). Значит
+скан-код, прилетевший ПОЗЖЕ этой проверки, доставался DSS и уезжал в его
+буфер — при этом **Rx-overrun не взводится**, потому что байт не потерян
+железом, он просто прочитан не тем владельцем. Отсюда и загадка
+исходного диагноза: бит залип, а `_kbdraw_overrun == 0`.
+
+**Измерено в MAME (сессия 2026-08-28, брейки + `totalcycles`):**
+
+| Что | Значение |
+|---|---|
+| окно от входа трамплина до чтения байта DSS | **738 тактов ≈ 34 мкс**, на КАЖДОМ кадровом прерывании (50/с) |
+| из них пролог самого DSS (`jp 0x38` → `.Handler` → `call INTx38_Handler` → 11 `push` → `call KEYSCAN`) | 481 такт ≈ 22 мкс |
+| цепочка кадровых хендлеров у SprPoP | пуста (`_irq_chain_n = 0`) — окно уже минимальное |
+| расчётная частота на железе при удержании клавиши (typematic 30 Гц = 60 байт/с) | потеря байта раз в ~10 с |
+
+Последняя строка сходится с полевым наблюдением пользователя: залипание
+стрелки ловилось на ур. 14, когда Кид долго бежит влево через несколько
+комнат.
+
+**Пойманный случай (MAME, старая сборка).** Стоп на `IN A,($18)` внутри
+`KEYSCAN` при `_kbdraw_active = 1`: DSS прочитал `A = 0x74` — make стрелки
+«вправо», при том что `_kbdraw_pending = 2` (EXT), то есть префикс `E0` уже
+разобрали МЫ. Посылка `E0 74` разорвана пополам между двумя владельцами
+канала. Злейший подвид — кража `E0` у break'а `E0 F0 6B`: тогда мы гасим
+бит в PLAIN-половине, а взведённый бит в EXT остаётся навсегда, и это ровно
+залипшая стрелка `0x6B`/`0x72` из исходного разбора.
+
+**Правка.** Пока raw-канал открыт, кадровый путь трамплина НЕ вызывает
+обработчик DSS вообще (приватный `RETI` вместо `jp 0x0038`) — окно ровно
+ноль. Промежуточный вариант «проверить FIFO перед chain'ом» пробовали и
+отвергли замером: он снимает лишь свою треть окна (738 → 481 такт), кражи
+продолжались. Цена принятого варианта: пока открыт raw, у DSS замирает
+кадровое обслуживание — опрос мыши и мигание текстового курсора (PoP не
+нужно ни то, ни другое; зафиксировано в ``).
+
+**Проверка (MAME 2026-08-28).** До правки брейк на входе `KEYSCAN` с
+условием «страница точно DSS + наш raw открыт» срабатывал мгновенно (50/с);
+после правки молчит, а положительный контроль — брейк на нашем приватном
+`RETI` — срабатывает сразу. ВАЖНО про методику: первый брейк без проверки
+сигнатуры байтов дал ЛОЖНОЕ срабатывание — в W0 периодически лежит
+драйверная страница DSS, и по адресу `0x0570` там своя команда.
+
+**Что осталось открытым.** Кража устранена, но потерять break всё ещё
+может настоящий Rx-overrun SIO (редкий: FIFO вычерпывает `kbd_raw_poll` из
+главного цикла). Значит вариант A из таблицы ниже — таймаут в циклах
+«ждём отпускания всех клавиш» — остаётся разумной страховкой от КЛАССА
+отказа «вечный клин», и его стоит сделать отдельно.
+
+## Исходный разбор (2026-08-24)
**Наблюдение (пользователь, 2026-08-24).** Нажат Shift+L (чит «следующий
уровень») — игра встала намертво: картинка прежнего уровня, реакции нет.
@@ -169,6 +228,13 @@ Rx-overrun не срабатывала. Гашение бита отладчи
> **Решение пользователя (2026-08-12): отложено до готовности всех уровней.**
> Механика работает, это вопрос ощущения, а не проходимости.
+> **Обновление 2026-08-28.** Гипотеза «дело в клавиатурном модуле, а не в
+> физике» подтвердилась ЧАСТИЧНО: найдена и устранена кража скан-кодов
+> обработчиком DSS (разбор — [KBD-STUCK-WAIT](#kbd-stuck-wait)). Украденный
+> make Shift'а или стрелки — это ровно «нажал, а не сработало», и на
+> железе при удержании клавиши такое случалось раз в ~10 с. Перепроверить
+> ощущение зацепа на новой сборке ДО того, как лезть в физику.
+
**Наблюдение (пользователь).** Вход на уровень 7: Кид влетает в комнату сверху
и, пролетая мимо края пола, МОЖЕТ за него зацепиться — но получается заметно
реже, чем в оригинале. «Похоже, это проблемы нашего клавиатурного модуля».
diff --git a/docs/size_baseline.tsv b/docs/size_baseline.tsv
index 510771f..a79bd53 100644
--- a/docs/size_baseline.tsv
+++ b/docs/size_baseline.tsv
@@ -1,5 +1,5 @@
# Эталон размеров _CODE (байт); обновление: python3 toolchain/size_check.py --update
-accfill 3786
+accfill 3783
accop 7067
argv 3431
assrtest 3847
@@ -9,15 +9,15 @@ banked 1056
bankedbg 1067
banklocl 4683
banktest 3767
-bgi_img 8680
-bgitest 3751
+bgi_img 8677
+bgitest 3748
bios_text 4461
blitperf 5860
blitw 4939
cat 913
-cblstream 5850
-cbltest 5996
-cblwav 6089
+cblstream 5860
+cbltest 6006
+cblwav 6099
conio 4605
conio2 3929
convbench 3508
@@ -28,12 +28,12 @@ fdmax 6023
filetest 10547
fpsdiv 4870
gets 509
-gfx_dbuf 5134
-gfx_demo 4117
+gfx_dbuf 5131
+gfx_demo 4114
gfxbanks 6044
hello 4167
hello2 4244
-irqtest 6680
+irqtest 6690
kbdpoll 1282
kbdraw 4911
ls 4835
@@ -44,7 +44,7 @@ openenv 6124
pageflip 6334
palfile 5406
ptime 5744
-rt_test 5187
+rt_test 5197
scroll 6744
seek 4188
simple 955
@@ -59,8 +59,7 @@ strtest 1340
text_palette 5024
timedir 5454
w0page 8831
-w3bankgfx 5240
-w3bgfx 5246
+w3bgfx 5243
w3big 3673
w3huge 3700
w3probe 3556
diff --git a/libc/include/kbd_raw.h b/libc/include/kbd_raw.h
index 2f31593..33f93b6 100644
--- a/libc/include/kbd_raw.h
+++ b/libc/include/kbd_raw.h
@@ -23,6 +23,16 @@
* cbl_open()/cbl_close() () — тот же приватный IM2-хук, не
* чейнящийся к DSS.
*
+ * КАДРОВОЕ ОБСЛУЖИВАНИЕ DSS НА ЭТО ВРЕМЯ ОСТАНОВЛЕНО. Пока raw открыт,
+ * трамплин НЕ вызывает обработчик прерывания DSS вообще: тот первым делом
+ * зовёт свой KEYSCAN, который вычерпывает FIFO SIO и уводит наши байты
+ * (потерянный make = несработавшее нажатие, потерянный break = залипшая
+ * клавиша; поймано в MAME 2026-08-28). Вместе с KEYSCAN замирают и
+ * остальные его кадровые дела — опрос мыши (Dss.Mouse.GetPackets) и
+ * мигание текстового курсора. ЗНАЧИТ: приложению, которому нужна мышь
+ * через DSS, raw-канал открывать нельзя (мышь встанет); клавиатурные
+ * события всё равно берутся из kbd_raw_down().
+ *
* КОДЫ КЛАВИШ: значения ниже — общеизвестный стандарт AT/PS-2 Scan
* Code Set 2 (не специфика Sprinter). TODO до использования в PoC:
* подтвердить в MAME, что реальный поток байт с SIO-A клавиатуры
diff --git a/libc/irq/_irq.h b/libc/irq/_irq.h
index 41a8ccb..921e141 100644
--- a/libc/irq/_irq.h
+++ b/libc/irq/_irq.h
@@ -30,10 +30,11 @@
#define _IRQ_VEC_BUF_SIZE (256 + 257)
/* Буфер под W2-копию кадрового трамплина (small/huge — _irq_tramp в
- * W1). С запасом над реальным размером трамплина (267 Б после
- * упаковки held-карты клавиатуры в биты, 2026-07-30 — проверять
- * __irq_tramp_end при правках, запас держать >= 60 Б). */
-#define _IRQ_TRAMP_BUF_SIZE 336
+ * W1). С запасом над реальным размером трамплина (310 Б после отказа
+ * звать обработчик DSS, пока открыт raw-канал, 2026-08-28 — проверять
+ * `grep 'A _CODE' libc/build/fast/irq/_irq_tramp.rel` при правках,
+ * запас держать >= 60 Б). */
+#define _IRQ_TRAMP_BUF_SIZE 384
extern uint8_t _irq_vec_buf[_IRQ_VEC_BUF_SIZE];
diff --git a/libc/irq/_irq_tramp.c b/libc/irq/_irq_tramp.c
index e8f04f7..6100449 100644
--- a/libc/irq/_irq_tramp.c
+++ b/libc/irq/_irq_tramp.c
@@ -13,7 +13,9 @@
* клавиши) в битовую карту `_kbdraw_down[]` (бит на код, 64 Б),
* и к DSS НЕ идём
* (private RETI — тот же паттерн, что и у CBL ниже). Пока raw
- * открыт, DSS клавиатуру не видит вообще (см. ).
+ * открыт, DSS клавиатуру не видит вообще (см. ) — и на
+ * кадровом пути тоже: пока raw открыт, обработчик DSS НЕ вызывается
+ * совсем (иначе его KEYSCAN крадёт наши байты — см. tr_frame_exit).
* - CBL (бит 7 порта 0xFE) — только при зарегистрированном хуке
* _irq_cbl_hook (иначе бит подтянут к 1); личное прерывание, RETI
* без чейна к DSS;
@@ -312,6 +314,39 @@ void _irq_tramp(void) __naked
ld a, (__irq_saved_w1) ; вернуть прерванную W1 перед chain
out (#_IRQ_W1_PORT), a
tr_frame_exit:
+ ;; --- ПОКА RAW ОТКРЫТ, В DSS НЕ ХОДИМ ВООБЩЕ ---
+ ;; Хвост кадрового пути уходил в DSS (0x0038), а его обработчик
+ ;; INTx38_Handler ПЕРВЫМ ДЕЛОМ зовёт KEYSCAN, и тот вычерпывает FIFO
+ ;; SIO досуха (docs/sources/Estex-DSS/DSS/DSS-MAIN.ASM,
+ ;; KEYINTER.ASM:KEYSCAN.RESCAN). Наша проверка «есть ли байт» стоит в
+ ;; НАЧАЛЕ трамплина, поэтому скан-код, прилетевший позже, доставался
+ ;; DSS и уезжал в ЕГО буфер — БЕЗ Rx-overrun. Украденный make =
+ ;; несработавшее нажатие, украденный break = залипшая навсегда
+ ;; клавиша при `_kbdraw_overrun == 0` (BUGS_OPEN KBD-STUCK-WAIT,
+ ;; GRAB-KBD-TIMING). Поймано в MAME 2026-08-28: DSS прочитал 0x74
+ ;; (make стрелки «вправо») при `_kbdraw_pending = EXT` — посылка
+ ;; `E0 74` разорвана пополам между двумя владельцами канала.
+ ;;
+ ;; Окно измерено брейками + totalcycles: от входа трамплина до
+ ;; чтения DSS — 738 тактов (~34 мкс), и открыто оно на КАЖДОМ
+ ;; кадровом прерывании (50/с). Из них 481 такт — пролог самого DSS
+ ;; (jp 0x38 → jp .Handler → call INTx38_Handler → 11 push → call
+ ;; KEYSCAN), то есть закрыть окно проверкой FIFO перед chain'ом
+ ;; нельзя: она снимает лишь треть (проверено там же).
+ ;;
+ ;; Поэтому пока raw открыт, кадровый путь заканчивается ПРИВАТНЫМ
+ ;; RETI и обработчик DSS не вызывается совсем — окно ровно ноль.
+ ;; ЦЕНА: на это время замирает кадровое обслуживание DSS —
+ ;; Dss.Mouse.GetPackets и мигание текстового курсора. Приложению с
+ ;; raw-клавиатурой (игре) не нужно ни то, ни другое; кому нужна мышь
+ ;; DSS одновременно с raw — не открывать raw (см. ).
+ ld a, (__kbdraw_active)
+ or a, a
+ jr Z, tr_chain_dss
+ pop af
+ ei
+ reti
+ tr_chain_dss:
pop af
.db 0xC3 ; jp 0x0038 (литерал — copy-safe)
.dw 0x0038