File size: 25,577 Bytes
78738de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PoolCoach — Gợi ý cú đánh 9-ball</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
  <!-- badge phiên bàn (QR, bàn giao 12) nằm trong header: hiện được cả
       desktop lẫn mobile mà không bao giờ đè lên canvas. Class RIÊNG
       .session-badge — không mượn class có event binding (bài học .btn-play). -->
  <h1>🎱 PoolCoach <span id="session-badge" class="session-badge hidden"></span></h1>
  <p>Kéo-thả bi theo bàn thật (mét) → <b>Gợi ý cú đánh</b><b>Đánh cú này</b> để chơi tiếp</p>
</header>

<main>
  <div id="table-wrap">
    <canvas id="table"></canvas>
    <div id="tooltip" class="hidden"></div>
  </div>

  <aside id="panel">
    <button id="sheet-close" class="sheet-close" aria-label="Đóng"></button>
    <div id="status" class="banner hidden"></div>

    <!-- A2b (feedback Danh 13/08 #2): Analyzer là tab ĐẦU + MẶC ĐỊNH khi mở
         app — tính năng chính của luận văn; tab Gợi ý lùi sau nhưng đầy đủ
         chức năng. HTML tĩnh phản ánh đúng trạng thái boot (Analyzer active,
         #analyzer hiện, .controls/.legend ẩn) khớp S.anMode=true trong
         app.js — MỘT nguồn sự thật cho trạng thái đầu, không có flash tab.
         Deep-link ?qr= (phiên bàn) vẫn vào thẳng tab Gợi ý — app.js xử lý.
         Tab Bài tập (F3) + Analyzer là LỚP THÊM: thoát tab là bàn + luồng
         gợi ý trả lại nguyên trạng (app.js chụp/khôi phục S.balls). -->
    <div class="tabs">
      <button id="tab-analyzer" class="tab active">Analyzer</button>
      <button id="tab-suggest" class="tab">Gợi ý</button>
      <button id="tab-drills" class="tab">Bài tập</button>
    </div>

    <!-- Broadcast Analyzer (bàn giao 24, đa cú từ A1): upload video 1..N cú
         → click 4 pocket trên frame đầu (overlay #an-overlay dưới cùng
         trang) → máy tự cắt cú → bảng danh sách cú. Mọi id/class .an-* là
         MỚI — không mượn class có event binding (bài học .btn-play 04/08).
         Luồng 1-cú GỠ ở A2b (feedback Danh 13/08): video 1 cú là trường
         hợp con của luồng đa cú — MỘT đường vào duy nhất. -->
    <section id="analyzer">
      <div class="an-intro">Tải lên video 9-ball (<b>1 hoặc nhiều cú</b>,
        một góc máy full-table ổn định, ≤ 20 phút, MP4/MOV) — máy tự tách
        từng cú đánh, đo quỹ đạo, V0, hướng φ, spin và các va chạm.</div>
      <!-- .an-cta chứ KHÔNG PHẢI .btn-go: .btn-go có binding recommend toàn
           cục + syncButtons ép label — mượn là nút này gọi ké engine và
           mọc nút "Đánh cú này" giữa tab Analyzer (bug smoke BG25). -->
      <button id="anv-pick" class="an-cta">🎬 Phân tích video (1 hoặc nhiều cú)</button>
      <div id="anv-result" class="hidden">
        <!-- Danh sách cú (A1) — lát A2 bọc vào #anv-list để trang chi tiết
             cú (#and-detail, §3.2 design) thế chỗ khi click một hàng.
             Mọi id/class .and-* là MỚI (bài học .btn-play). -->
        <div id="anv-list">
          <div id="anv-progressline" class="anv-progressline"></div>
          <div id="anv-warnings"></div>
          <!-- Xuất toàn danh sách cú (A2 phần 5): CSV UTF-8 BOM (Excel
               không vỡ tiếng Việt) + JSON — cùng nguồn với bảng. Href do
               JS điền theo video đang xem. -->
          <div class="btn-row anv-export hidden">
            <a id="anv-csv" class="btn-mini and-link" download>⬇ Xuất CSV</a>
            <a id="anv-json" class="btn-mini and-link" download>⬇ Xuất JSON</a>
          </div>
          <div class="anv-tablewrap">
            <table id="anv-table" class="anv-table">
              <thead><tr>
                <th>#</th><th></th><th>V0</th><th>φ</th>
                <th>Spin (a/b)</th><th>Tin cậy</th><th>RMSE</th>
                <th>Trạng thái</th>
              </tr></thead>
              <tbody id="anv-tbody"></tbody>
            </table>
          </div>
          <div class="an-note">Click một hàng <b>đã xong</b> để mở trang
            chi tiết cú (overlay + resim chồng + bảng thông số; JSON thô
            có link trong đó). V0/φ/spin lấy từ cột model (ShotNet) khi có
            — nhãn "bi" là tốc độ bi đo giải tích, hai thước khác nhau.
            Cột RMSE = khớp resim pooltool ↔ track quan sát (mm, bộ params
            model ShotNet trừ khi ghi khác). Cú hàng xám là cú máy không
            phân tích được, kèm lý do. Cú xám
            "⚑ nghi không phải cú đánh" là van hiển thị từ tín hiệu cú tự
            khai (V0 / motion_start / coverage) — không bị lọc bỏ, vẫn
            click xem được.</div>
        </div>
        <!-- Trang chi tiết cú (lát A2 phần 4, §3.2 design): overlay.mp4 +
             bàn metric quan sát (nét liền, vẽ trên canvas bàn) vs resim
             (nét đứt) + bảng 2 cột + nút "Đánh lại cú" (animateShot() sẵn
             có — KHÔNG animation mới). -->
        <div id="and-detail" class="hidden">
          <div class="btn-row">
            <button id="and-back" class="btn-mini">← Danh sách cú</button>
            <a id="and-json" class="btn-mini and-link" target="_blank">JSON thô</a>
            <a id="and-download" class="btn-mini and-link hidden">⬇ Tải mp4</a>
          </div>
          <h3 id="and-title"></h3>
          <video id="and-video" class="and-video hidden" controls playsinline></video>
          <div id="and-video-missing" class="an-note hidden">Cú này chưa có
            overlay.mp4 — kết quả thuộc lần chạy trước A2 (clip đã xoá,
            không render hồi tố được) hoặc render lỗi (xem cảnh báo).</div>
          <!-- A2b phần 4 (Danh chốt 13/08 tối): "Đánh lại cú" animate theo
               quỹ đạo QUAN SÁT — cú thật sống lại, luôn đúng bất kể model. -->
          <button id="and-replay" class="an-cta" disabled>▶ Đánh lại cú (quỹ đạo quan sát)</button>
          <!-- Widget thông tin cú khi đánh lại (§3.2): bi cái + chấm điểm
               đầu cơ (a/b) + thanh lực V0 — tái dụng MARKUP + CSS style-only
               (.viz*) của #shotviz tab gợi ý; id + class tương tác RIÊNG
               (.and-spin-dot / .and-power-bar) vì .spin-dot/.power-bar bị
               initPowerBar + updateShotViz quét toàn cục (bài học .btn-play).
               Số từ bộ SHOTNET — analytic không có a/b; nhãn bộ ghi thẳng. -->
          <div id="and-shotviz" class="viz hidden">
            <div class="viz-spin">
              <svg id="and-spin-ball" viewBox="0 0 100 100" aria-label="điểm chạm bi cái (model)">
                <defs>
                  <radialGradient id="and-cuegrad" cx="35%" cy="30%" r="80%">
                    <stop offset="0%" stop-color="#ffffff"/>
                    <stop offset="100%" stop-color="#c9c9c9"/>
                  </radialGradient>
                </defs>
                <circle cx="50" cy="50" r="46" fill="url(#and-cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/>
                <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
                <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
                <circle class="and-spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/>
              </svg>
              <div class="viz-cap" id="and-spin-cap"></div>
            </div>
            <div class="viz-power">
              <div class="viz-lbl">Lực <b id="and-power-val"></b></div>
              <div id="and-power-bar" class="and-power-bar"></div>
              <div class="viz-scale"><span>0.5</span><span>m/s</span><span>8.0</span></div>
              <div class="viz-cap" id="and-viz-label"></div>
            </div>
          </div>
          <div id="and-suspect" class="an-warn hidden"></div>
          <!-- Lớp resim thành toggle, MẶC ĐỊNH TẮT (path quan sát ↔ resim
               đang lệch V0-thang, hiện cả hai thì rối — feedback #3). Bật
               mặc định lại = một hằng AND_RESIM_DEFAULT_ON trong app.js. -->
          <label id="and-resim-row" class="and-toggle hidden">
            <input type="checkbox" id="and-resim-toggle">
            So với mô phỏng (vẽ nét đứt resim + RMSE)
          </label>
          <div id="and-resim" class="an-note hidden"></div>
          <div id="and-warnings"></div>
          <table id="and-metrics" class="an-metrics"></table>
          <div id="and-caveat" class="an-caveat hidden">Model train thuần mô
            phỏng (P2), chưa tinh chỉnh trên video thật.</div>
          <div class="an-note">Bàn bên trái: <b>nét liền tô màu theo thời
            gian</b> = track quan sát; bi có số/màu theo BallID khi kết quả
            có nhận diện (kết quả cũ hiện bi xám). "Đánh lại cú" chạy lại
            chính quỹ đạo quan sát đó bằng animation của app gợi ý. Bật
            "So với mô phỏng" mới vẽ <b>nét đứt trắng</b> = quỹ đạo resim
            từ params suy được (bộ ghi ở dòng RMSE).</div>
        </div>
      </div>
    </section>

    <section id="drills" class="hidden">
      <!-- khu "Tiến bộ" (bàn giao 12): chỉ hiện khi có phiên bàn + DB —
           tổng hợp pass/fail theo drill của ĐÚNG phiên này. Không phiên /
           503 → ẩn khu, hiện dòng nhắc. Class riêng .drill-progress. -->
      <div id="drill-progress" class="drill-progress hidden">
        <b>Tiến bộ phiên này</b>
        <div id="drill-progress-body"></div>
      </div>
      <div id="drill-progress-hint" class="drill-note hidden">Vào bàn bằng
        mã QR để lưu và xem tiến bộ tập.</div>
      <div id="drill-list"></div>
      <div id="drill-detail" class="hidden">
        <div class="btn-row">
          <button id="drill-back" class="btn-mini">← Danh sách</button>
          <button id="drill-reset" class="btn-mini">↺ Xếp lại bi</button>
        </div>
        <h3 id="drill-title"></h3>
        <div id="drill-goal"></div>
        <div class="row drill-round">
          <span class="lbl">Vòng tập</span>
          <b id="drill-count"></b>
        </div>
        <div class="btn-row">
          <!-- class RIÊNG .btn-pass (không phải .btn-play): init cũ bind
               playShot cho mọi .btn-play — dùng ké class đó là bấm "Đạt"
               kích luôn animation cú gợi ý (bug bắt được trong smoke 04/08) -->
          <button id="drill-pass" class="btn-pass">✔ Đạt</button>
          <button id="drill-fail" class="btn-fail">✘ Trượt</button>
        </div>
        <div id="drill-summary" class="hidden"></div>
        <button id="drill-retry" class="btn-mini hidden">↻ Tập vòng mới</button>
        <div id="drill-note" class="drill-note hidden">Chế độ demo — tiến bộ
          không được lưu (server chưa có cơ sở dữ liệu).</div>
        <button id="drill-suggest" class="btn-go" disabled>Xem gợi ý</button>
      </div>
    </section>

    <section class="controls hidden">
      <!-- Bộ chọn engine GỠ 31/07/2026 (BRIEF việc B): app chỉ còn MỘT đường
           chọn cú — ZonePlanner V2 (tiêu chí design §8). Server tự chạy nó,
           không có gì để chọn. Thời gian thật của từng lần gợi ý vẫn hiện ở
           #searchinfo, đo bởi chính server. -->
      <div class="row">
        <span class="lbl">Bi trên bàn</span>
        <span id="panel-target" class="target-badge">→ bi 1</span>
      </div>
      <div class="row" id="ball-chips">
        <button class="chip" data-bid="1">1</button>
        <button class="chip" data-bid="2">2</button>
        <button class="chip" data-bid="3">3</button>
        <button class="chip" data-bid="4">4</button>
        <button class="chip" data-bid="5">5</button>
        <button class="chip" data-bid="6">6</button>
        <button class="chip" data-bid="7">7</button>
        <button class="chip" data-bid="8">8</button>
        <button class="chip" data-bid="9">9</button>
      </div>
      <div class="row btn-row">
        <button id="btn-preset" class="btn-mini">Bàn mẫu</button>
        <button id="btn-undo" class="btn-mini" disabled>↩ Undo</button>
        <button id="btn-tv" class="btn-mini">📺 Ghép TV</button>
      </div>
      <!-- Scan ảnh (F2 MVP, bàn giao 18): id riêng + class style-only —
           không mượn class có event binding (bài học .btn-play 04/08) -->
      <div class="row btn-row">
        <button id="btn-scan" class="btn-mini" disabled>📷 Scan ảnh bàn</button>
      </div>
      <!-- form Ghép TV (bàn giao 15): class RIÊNG .tv-pair — không mượn
           class có event binding toàn cục (bài học .btn-play 04/08) -->
      <div id="tv-pair" class="tv-pair hidden">
        <input id="tv-code" inputmode="numeric" autocomplete="one-time-code"
               maxlength="6" placeholder="Mã 6 số trên TV">
        <button id="tv-pair-go">Ghép</button>
      </div>
      <button id="btn-go" class="btn-go" disabled>Gợi ý cú đánh</button>
      <!-- auto-play để NGOÀI #result: #result ẩn tới khi bấm Gợi ý lần đầu,
           mà "tự chơi hết ván" lại là nút muốn bấm ngay lúc mở trang -->
      <button id="btn-auto" class="btn-mini btn-auto" disabled>▶ Tự chơi hết ván</button>
    </section>

    <section id="result" class="hidden">
      <div id="shotviz" class="viz hidden">
        <div class="viz-spin">
          <svg id="spin-ball" viewBox="0 0 100 100" aria-label="điểm chạm bi cái">
            <defs>
              <radialGradient id="cuegrad" cx="35%" cy="30%" r="80%">
                <stop offset="0%" stop-color="#ffffff"/>
                <stop offset="100%" stop-color="#c9c9c9"/>
              </radialGradient>
            </defs>
            <circle cx="50" cy="50" r="46" fill="url(#cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/>
            <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
            <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
            <circle class="spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/>
          </svg>
          <div class="viz-cap" id="spin-cap"></div>
        </div>
        <div class="viz-power">
          <div class="viz-lbl">Lực <b id="power-val"></b></div>
          <div id="power-bar" class="power-bar"></div>
          <div class="viz-scale"><span>0.5</span><span>m/s</span><span>4.0</span></div>
        </div>
      </div>
      <pre id="describe"></pre>
      <div class="btn-row play-row">
        <button id="btn-play" class="btn-play" disabled>Đánh cú này</button>
      </div>
      <div id="alts"></div>
      <div id="searchinfo"></div>
    </section>

    <section class="legend hidden">
      <span><i class="ring target"></i> bi mục tiêu (số nhỏ nhất)</span>
      <span><i class="ring"></i> lỗ đích</span>
      <span><i class="ring lowconf"></i> bi scan chưa chắc — kiểm lại</span>
      <span>Tắt bi bằng ô số 1-9 để dựng bàn giữa ván</span>
    </section>
  </aside>
</main>

<div id="backdrop"></div>

<!-- Scan ảnh (F2 MVP, bàn giao 18): overlay riêng, mọi class .scan-* là
     class MỚI (bài học .btn-play — không mượn class có event binding).
     Ảnh đã resize hiện trên canvas riêng; người dùng chấm 4 GÓC MẶT NỈ theo
     thứ tự #scan-hint chỉ dẫn từng bước; góc lưu sessionStorage — ảnh sau
     chụp cùng khung hình thì khỏi chấm lại (vẫn có nút Chấm lại góc). -->
<div id="scan-overlay" class="scan-overlay hidden">
  <div class="scan-box">
    <div class="scan-head">
      <b>📷 Scan ảnh bàn</b>
      <button id="scan-close" class="scan-x" aria-label="Đóng"></button>
    </div>
    <div id="scan-hint" class="scan-hint"></div>
    <canvas id="scan-canvas"></canvas>
    <div class="btn-row scan-actions">
      <button id="scan-redo" class="btn-mini">↺ Chấm lại góc</button>
      <!-- .an-cta chứ KHÔNG PHẢI .btn-go (BG26, cùng bug 2 nút Analyzer
           9fa4c18): .btn-go có binding recommend() toàn cục + syncButtons
           ép label/disabled — mượn là "Quét thế bi" gọi ké engine chạy nền
           và giành label với syncScanUi. -->
      <button id="scan-go" class="an-cta scan-go" disabled>Quét thế bi</button>
    </div>
  </div>
</div>
<input type="file" id="scan-file" accept="image/*" class="hidden">

<!-- Analyzer (bàn giao 24): overlay chấm 4 POCKET GÓC trên frame ĐẦU của
     chính clip vừa chọn (FE tự trích bằng <video> — không round-trip server).
     Container mượn class STYLE-ONLY .scan-overlay/.scan-box (không có event
     binding nào bind theo các class đó — bài học .btn-play chỉ cấm mượn class
     CÓ binding); mọi phần tử tương tác mang id .an-* riêng. -->
<div id="an-overlay" class="scan-overlay hidden">
  <div class="scan-box">
    <div class="scan-head">
      <b>🎬 Phân tích video — 4 pocket góc</b>
      <button id="an-close" class="scan-x" aria-label="Đóng"></button>
    </div>
    <div id="an-hint" class="scan-hint"></div>
    <!-- BG31: chuẩn ĐỘ CAO điểm click — bù độ cao tâm bi (worker) tính cho
         mặt phẳng VẢI; click cao hơn (mép gỗ/miệng lỗ, z_c ≈ +21mm phiên
         pilot) làm bù thành quá tay — BG30 đã đo tác hại. Note TĨNH, class
         .an-note MỚI style-only (bài học .btn-play: không mượn class có
         binding). Hình = mặt cắt ngang băng: gỗ + miệng lỗ trên cao, băng
         cao su nghiêng xuống, mũi nose chạm mặt vải là điểm ĐÚNG. -->
    <div id="an-note" class="an-note">
      <svg viewBox="0 0 168 62" aria-hidden="true">
        <rect x="2" y="46" width="164" height="8" rx="1" fill="#2e7d46"/>
        <polygon points="2,16 46,16 76,46 2,46" fill="#1f5c33"/>
        <rect x="2" y="6" width="44" height="10" rx="2" fill="#8a5a2b"/>
        <ellipse cx="15" cy="11" rx="8" ry="4.5" fill="#141414"/>
        <text x="28" y="14" fill="#ff6b6b" font-size="10.5">✗ miệng lỗ</text>
        <circle cx="76" cy="46" r="5.5" fill="none" stroke="#39d353"
                stroke-width="2"/>
        <text x="86" y="50" fill="#39d353" font-size="10.5">✓ mép nose vải</text>
      </svg>
      <span>Chấm đúng <b>mép nose vải</b> — nơi mũi băng cao su gặp mặt vải;
      <b>KHÔNG</b> chấm miệng lỗ trên mặt gỗ.</span>
    </div>
    <canvas id="an-canvas"></canvas>
    <!-- Chỉ hiện ở bản deploy CPU (health.mode == "local") — JS bật/tắt. -->
    <div id="an-cpu-warn" class="an-note hidden"><b>Server này phân tích
      bằng CPU</b> (bản demo online, không GPU) nên chậm — đo được:
      <b>clip 5,5 giây mất ~26 giây</b>, một rack 5 phút ước
      <b>10–12 phút</b>. Bảng cú <b>hiện dần từng cú</b> nên xem được ngay
      khi cú đầu xong; đóng tab cũng không mất kết quả, mở lại link là còn.
      Muốn xem ngay thì bảng đang có sẵn <b>video mẫu đã phân tích</b>.</div>
    <div class="btn-row scan-actions">
      <button id="an-redo" class="btn-mini">↺ Chấm lại</button>
      <!-- Xoay nhãn (BRIEF 14/08 việc A): máy đoán CẶP BĂNG NGẮN bằng
           heuristic bất đối xứng độ dài (poolcoach_cv/autocorner) — đoán sai
           thì xoay ①②③④ một nhịp là xong, khỏi chấm lại 4 điểm. -->
      <button id="an-rotate" class="btn-mini" disabled>⟳ Xoay nhãn</button>
      <!-- .an-cta thay .btn-go — cùng lý do #anv-pick (binding recommend +
           syncButtons ép label/disabled đè lên syncAnUi). -->
      <button id="an-go" class="an-cta scan-go" disabled>Phân tích</button>
    </div>
  </div>
</div>
<input type="file" id="an-file" accept="video/mp4,video/quicktime,.mp4,.mov" class="hidden">

<!-- quick bar mobile: options + spin/lực tóm tắt + nút Gợi ý, không cần scroll.
     Quickbar nằm NGOÀI #panel (container 3 tab) — cố ý, vì nó phải nổi cố định
     đáy màn hình kể cả khi sheet đóng. Hệ quả: setTab() KHÔNG ẩn nó theo tab,
     nên phần thuộc tab Gợi ý trong đây phải được ẩn TAY (bug Danh 14/08: mở
     app trên điện thoại vào thẳng tab Analyzer mà đáy vẫn là UI gợi ý).
     Chia đôi rạch ròi:
       .qb-settings = ĐIỀU HƯỚNG, luôn hiện — trên mobile #panel là
                      display:none tới khi "Chi tiết ▴" mở sheet, ẩn hàng này
                      là nhốt người dùng trong tab hiện tại, không còn lối ra;
       #qb-target + .qb-main = UI tab GỢI Ý (badge bi mục tiêu, chấm đầu cơ,
                      thanh lực, nút Gợi ý/Đánh) — ẩn khi ở tab Analyzer.
     Trạng thái tĩnh dưới đây khớp boot (Analyzer active), đúng nếp
     .controls/.legend ở trên: MỘT nguồn sự thật, không flash tab. -->
<nav id="quickbar">
  <div class="qb-row qb-settings">
    <span id="qb-target" class="target-badge hidden">→ bi 1</span>
    <!-- lối vào tab Bài tập trên mobile (BRIEF 04/08: chỉ thêm lối vào,
         không đụng gì khác của quickbar) -->
    <button id="qb-drills">🎯 Bài tập</button>
    <button id="qb-detail">Chi tiết ▴</button>
  </div>
  <div class="qb-row qb-main hidden">
    <svg id="qb-spin" viewBox="0 0 100 100" aria-label="điểm chạm bi cái">
      <defs>
        <radialGradient id="qb-cuegrad" cx="35%" cy="30%" r="80%">
          <stop offset="0%" stop-color="#ffffff"/>
          <stop offset="100%" stop-color="#c9c9c9"/>
        </radialGradient>
      </defs>
      <circle cx="50" cy="50" r="46" fill="url(#qb-cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/>
      <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
      <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
      <circle class="spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/>
    </svg>
    <div class="qb-mid">
      <div class="power-bar qb-power"></div>
      <div id="qb-info">Đặt bi rồi bấm Gợi ý</div>
    </div>
    <button id="qb-go" class="btn-go" disabled>Gợi ý</button>
    <button id="qb-play" class="btn-play" disabled>Đánh</button>
  </div>
  <!-- Hàng quickbar của tab ANALYZER (14/08, Danh yêu cầu) — đối xứng với
       .qb-main ở trên: thông số cú đang mở + nút đánh lại, ngay đáy màn hình.
       Vì sao cần: trên mobile #panel là bottom sheet CHE BÀN, nên bấm "Đánh
       lại cú" trong sheet là animate sau lưng người dùng; hàng này cho thấy
       số trong lúc bi chạy trên bàn (đúng cách tab Gợi ý vẫn làm).
       Chỉ hiện khi ĐANG mở một cú (ANV.detail) ở tab Analyzer — ở màn danh
       sách thì không có gì để hiện.
       Class tương tác RIÊNG .qban-*; chấm spin + thanh lực CỐ Ý mang class
       .and-spin-dot/.and-power-bar của widget trang chi tiết để initPowerBar
       và fillAndShotviz nuôi CẢ HAI từ một nguồn số — không mượn
       .spin-dot/.power-bar/.btn-play/.btn-go (bị updateShotViz + syncButtons
       quét toàn cục, bài học .btn-play 04/08). -->
  <div class="qb-row qb-an hidden">
    <svg id="qban-spin" viewBox="0 0 100 100" aria-label="điểm chạm bi cái (model)">
      <defs>
        <radialGradient id="qban-cuegrad" cx="35%" cy="30%" r="80%">
          <stop offset="0%" stop-color="#ffffff"/>
          <stop offset="100%" stop-color="#c9c9c9"/>
        </radialGradient>
      </defs>
      <circle cx="50" cy="50" r="46" fill="url(#qban-cuegrad)" stroke="#1c1c1c" stroke-width="1.6"/>
      <line x1="50" y1="7" x2="50" y2="93" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
      <line x1="7" y1="50" x2="93" y2="50" stroke="#9a9a9a" stroke-width="1" stroke-dasharray="3 4"/>
      <circle class="and-spin-dot" cx="50" cy="50" r="6.5" fill="#e02020" stroke="#7a0000" stroke-width="1.3"/>
    </svg>
    <div class="qb-mid">
      <div class="and-power-bar qb-power qban-bar"></div>
      <div id="qban-info"></div>
    </div>
    <button id="qban-replay" class="an-cta qban-replay" disabled>▶ Đánh lại</button>
  </div>
</nav>

<div id="toast" class="hidden"></div>
<script src="app.js"></script>
</body>
</html>