/* CSS riêng cho trang Ôn Luyện (practice.html) — khung xem đôi "tờ insert | câu hỏi" và thanh kéo
   chỉnh độ rộng. Phần dùng chung nằm ở assets/css/theme.css.

   Vì sao không làm hết bằng class Tailwind: độ rộng hai bên là một con số CHẠY (người dùng kéo),
   nên phải đi qua biến CSS; và con số đó chỉ có nghĩa khi màn hình đủ rộng để xếp hai bên cạnh
   nhau — dưới 1024px hai khung xếp dọc và độ rộng phải trả về 100%. Viết bằng style inline thì
   không có chỗ nào đặt được điều kiện màn hình. */

.khung-chia {
    display: flex;
    flex-direction: column;
    gap: 0;
}

/* Khung tờ insert. Dưới lg: nằm trên, cao cố định để phần câu hỏi vẫn thấy được ngay dưới.
   Từ lg: nằm bên trái, rộng đúng --rong-insert (mặc định 50%, người dùng kéo đổi). */
.khung-chia__insert {
    width: 100%;
    height: 46vh;
    min-height: 220px;
    overflow: auto;
    overscroll-behavior: contain;
    /* Áo mới 25/08/2026: dải lạnh + viền ấm + bo 14px theo hợp đồng toàn site (cuối theme.css).
       overflow:auto sẵn có nên góc bo tự cắt gọn cả thanh tiêu đề sticky bên trong. */
    background: var(--ao-nen-lanh);
    border: 1px solid var(--ao-vien);
    border-radius: var(--ao-r-nho);
}

.khung-chia__cau-hoi {
    flex: 1 1 auto;
    min-width: 0;
    overflow: auto;
    overscroll-behavior: contain;
    padding-top: 0.75rem;
}

/* Thanh kéo. Dưới lg là một vạch ngang trang trí (không kéo được — xếp dọc thì kéo ngang vô nghĩa),
   từ lg mới thành tay kéo thật. */
.khung-chia__tay-keo {
    display: none;
}

@media (min-width: 1024px) {
    .khung-chia {
        flex-direction: row;
        height: 76vh;
    }

    .khung-chia__insert {
        width: var(--rong-insert, 50%);
        height: auto;
        flex: 0 0 auto;
    }

    .khung-chia__cau-hoi {
        padding-top: 0;
        padding-left: 0.75rem;
    }

    .khung-chia__tay-keo {
        display: block;
        flex: 0 0 10px;
        position: relative;
        cursor: col-resize;
        background: transparent;
        border: 0;
        padding: 0;
        touch-action: none; /* để pointermove không bị trình duyệt nuốt thành cuộn trang */
    }

    /* Đợt 4 mục 4.4: con trỏ NGỮ NGHĨA lúc đang kéo. Không có luật này thì ngay khi bấm giữ,
       con trỏ nhảy về mũi tên mặc định và người dùng tưởng đã tuột tay khỏi thanh chia. */
    .khung-chia__tay-keo:active {
        cursor: col-resize;
    }

    /* Vạch mảnh ở giữa vùng bấm 10px — vùng bấm phải rộng hơn vạch thì mới trúng chuột dễ. */
    .khung-chia__tay-keo::before {
        content: '';
        position: absolute;
        top: 0;
        bottom: 0;
        left: 50%;
        width: 2px;
        transform: translateX(-50%);
        background: var(--stone-200);
        transition: background-color 0.15s ease;
    }

    .khung-chia__tay-keo:hover::before,
    .khung-chia__tay-keo:focus-visible::before,
    .khung-chia__tay-keo[data-dang-keo="1"]::before {
        background: var(--navy-500);
        width: 3px;
    }

    .khung-chia__tay-keo:focus-visible {
        outline: 2px solid var(--navy-500);
        outline-offset: -2px;
    }
}

/* Mỗi trang PDF của tờ insert là một canvas. Nền trắng + viền mảnh để phân biệt ranh giới trang
   khi cuộn liên tục nhiều trang. */
.trang-insert {
    display: block;
    width: 100%;
    height: auto;
    background: var(--trang);
    border: 1px solid var(--stone-200);
    margin: 0 auto 0.5rem;
}

/* Đang kéo thanh chia: cấm bôi đen chữ trong cả trang, nếu không con trỏ quét qua câu hỏi sẽ
   bôi xanh cả khung trong lúc kéo.

   cursor thêm 28/08/2026 (đợt 4 mục 4.4): một mình .khung-chia__tay-keo:active ở trên KHÔNG đủ.
   batDauKeoKhung dùng setPointerCapture, mà pointer capture chỉ đổi nơi nhận SỰ KIỆN chứ không đổi
   nơi trình duyệt lấy con trỏ: con trỏ vẫn đọc theo phần tử nằm dưới nó. Kéo nhanh là con trỏ vượt
   khỏi vạch 10px và biến thành con trỏ chữ trên ảnh đề bên cạnh, đúng cảm giác "tuột tay". */
body.dang-keo-khung * {
    user-select: none !important;
    cursor: col-resize !important;
}


/* ---- Mở tờ insert: hàng xem câu TRÀN ra hết bề ngang cửa sổ (24/08/2026) ----
   Khung nội dung của trang rộng max-w-7xl (1280px, trừ padding còn 1216px) để mép trái mọi khung
   trắng thẳng hàng với logo VPath và mép phải thẳng hàng với nút tài khoản. Bề ngang đó vừa đủ cho
   MỘT trang đề, nhưng mở tờ insert là phải chứa HAI trang A4 cạnh nhau: chia đôi 1216px còn khoảng
   560px mỗi bên, tức mỗi trang co về ~70% cỡ thật (A4 ở 96dpi rộng 794px) và chữ trong đề nhỏ tới
   mức phải ghé sát màn hình mới đọc nổi.
   Vì vậy RIÊNG lúc mở tờ insert, hàng "danh sách + khung xem" được phép tràn khỏi khung nội dung ra
   hết bề ngang cửa sổ, chỉ chừa 2rem mỗi mép cho khớp padding lg:px-8 của trang. Đóng tờ insert là
   mọi thứ về lại đúng hàng với header như cũ. Trên màn 1920px, mỗi bên khung đôi được ~890px, tức
   gần đúng cỡ thật của một trang A4.
   Chỉ từ 1024px: hẹp hơn thì hai khung xếp dọc (xem .khung-chia ở trên), tràn ngang không thêm được
   gì mà còn phá bố cục một cột.
   Vì sao 100vw an toàn ở đây: 100vw tính CẢ thanh cuộn dọc (~15px khi hệ điều hành để thanh cuộn
   luôn hiện), nhưng phần trừ đi là 4rem = 64px > 15px nên khung vẫn hẹp hơn vùng nhìn thật, không
   đẻ ra thanh cuộn ngang. margin-left dùng 50% (nửa khung nội dung) trừ 50vw (nửa cửa sổ) để kéo
   mép trái về đúng 2rem tính từ mép cửa sổ, đúng cho cả khi cửa sổ hẹp hơn 1280px. */
@media (min-width: 1024px) {
    .hang-xem--rong {
        width: calc(100vw - 4rem);
        max-width: calc(100vw - 4rem);
        margin-left: calc(50% - 50vw + 2rem);
    }
}

/* ---- Thanh công cụ của câu hỏi: GHIM vào đỉnh khung xem (lỗi A5, kiểm định 22/08) ----
   Một câu 9618 dài tới ba màn hình; đọc tới cuối là Question / Mark Scheme / Insert Paper / mốc
   điểm / hai nút đánh dấu đã trôi hết khỏi màn hình, muốn xem đáp án phải cuộn ngược lên đầu.

   Vì sao ghim Ở ĐÂY (con trực tiếp của khung trắng) chứ không ghim bên trong .khung-chia__cau-hoi:
   khung đó có overflow:auto nên position:sticky trong nó sẽ dính vào mép khung CON chứ không phải
   mép màn hình, mà ở chế độ tờ insert thanh công cụ còn phải nằm trên CẢ HAI bên.

   top: 5rem vì header trang đã sticky top-0 với chiều cao h-20 = 80px ở mọi bề rộng (h-20 không có
   biến thể responsive) — để top: 0 thì thanh công cụ chui xuống dưới header và mất một nửa. Không
   dùng lớp .top-20 của Tailwind: bản build sẵn assets/css/tailwind.ea465e5f.css KHÔNG có lớp đó, thêm vào là
   bắt cả dự án chạy lại npm run build:css chỉ vì một con số.

   z-index 20 < 40 của header: thanh công cụ phải trôi XUỐNG DƯỚI header chứ không đè lên. Dropdown
   Cấp Học / Môn Học dùng z-50 nên vẫn phủ lên được. */
.thanh-cong-cu-cau {
    position: sticky;
    top: 5rem;
    z-index: 20;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    /* Bù đúng p-4 (1rem) của khung trắng bao ngoài để dải nền chạy hết bề ngang: không bù thì ảnh
       câu hỏi cuộn qua bên dưới sẽ lộ ra ở hai mép 1rem. margin-bottom 1rem thay cho lớp mb-4 vừa gỡ. */
    margin: -1rem -1rem 1rem;
    padding: 0.75rem 1rem;
    background: var(--trang);
    border-bottom: 1px solid var(--stone-200);
    /* Áo mới 25/08/2026: khung trắng bao ngoài nay bo 14px, mà thanh này tràn sát mép khung
       (margin âm ở trên) — không bo hai góc trên theo (trừ 1px viền) thì mảng nền trắng vuông
       của nó đè mất cung viền của khung lúc trang chưa cuộn. Lúc ĐANG ghim giữa trang, hai góc
       bo để lộ hai khe ~13px cho ảnh câu hỏi ló qua — chấp nhận được so với viền gãy thường trực. */
    border-radius: calc(var(--ao-r-nho) - 1px) calc(var(--ao-r-nho) - 1px) 0 0;
}

/* Mở tờ insert thì BỎ ghim, ở MỌI bề rộng. Bản đầu chỉ bỏ ghim từ 1024px với lý do "dưới mốc đó hai
   khung xếp dọc nên trang cuộn thật" — soi lại thì chính chỗ đó mới đau: trên điện thoại tờ insert cao
   46vh còn thanh công cụ ghim chiếm tới hơn một phần ba màn hình, nên nó che mất thanh tiêu đề của tờ
   insert (số trang + ba nút zoom/đóng) và cú bấm vào nút zoom rơi trúng nút Mark Scheme nằm dưới.
   PHẢI đứng SAU .thanh-cong-cu-cau ở trên: hai selector cùng độ ưu tiên (0,1,0), thứ tự trong file mới
   là thứ quyết định. */
.thanh-cong-cu-cau--khong-ghim {
    position: static;
}

/* ---- Cụm "Câu trước / Câu tiếp theo" (lỗi A5) ---- */
.dieu-huong-cau {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.dieu-huong-cau__nut {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    padding: 0.5rem 0.875rem;
    border: 1px solid var(--ao-vien);
    background: var(--trang);
    border-radius: var(--ao-r-vien-thuoc); /* áo mới 25/08/2026: nút = viên thuốc (xem cuối theme.css) */
    font-size: 0.75rem;
    font-weight: 700;
    color: var(--navy-900);
    white-space: nowrap;
    transition: border-color 0.15s ease, background-color 0.15s ease;
}

.dieu-huong-cau__nut:hover:not(:disabled) {
    border-color: var(--navy-500);
    background: var(--ao-nen-lanh);
}

/* Hết danh sách (câu đầu / câu cuối): làm mờ chứ KHÔNG giấu nút — giấu thì bố cục nhảy chỗ mỗi lần
   sang câu, hai nút cứ trượt qua trượt lại. Vòng focus bàn phím dùng luật chung ở theme.css. */
.dieu-huong-cau__nut:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.dieu-huong-cau__vi-tri {
    font-size: 0.6875rem;
    font-weight: 700;
    color: var(--stone-600);
    white-space: nowrap;
}

/* Bản GỌN nằm trong thanh công cụ đã ghim — nhãn ngắn để còn chỗ cho Question / Mark Scheme. */
.dieu-huong-cau--gon .dieu-huong-cau__nut {
    padding: 0.5rem 0.75rem;
}

/* Dưới 640px thì GIẤU hẳn bản gọn. Màn hình điện thoại chỉ đủ chỗ cho Question / Mark Scheme và hai
   nút đánh dấu; nhét thêm cụm điều hướng là thanh công cụ dàn thành ba hàng và ăn hơn một phần ba
   chiều cao màn hình — ghim một thanh như thế thì hại nhiều hơn lợi. Cụm ĐÁY vẫn còn nguyên, và trên
   màn hẹp nó mới là chỗ tay người dùng với tới thuận nhất. */
@media (max-width: 639px) {
    .dieu-huong-cau--gon {
        display: none;
    }
}

/* Bản ĐÁY: một dải trắng riêng ngay dưới khung xem, hai nút đẩy ra hai mép. Là dải RIÊNG (nằm ngoài
   khung trắng của câu) để dùng chung được cho cả câu đang bị khoá — vào câu khoá vẫn còn lối đi tiếp. */
.dieu-huong-cau--day {
    justify-content: space-between;
    margin-top: 0.75rem;
    padding: 0.75rem 1rem;
    background: var(--trang);
    border: 1px solid var(--ao-vien);
    border-radius: var(--ao-r-nho); /* áo mới 25/08/2026: dải trắng riêng cũng là một thẻ -> bo 14px */
}

/* Từng mục trong dropdown Cấp Học / Môn Học. Đi bằng mũi tên thì PHẢI nhìn ra mình đang đứng ở mục
   nào — hover không giúp gì cho người dùng bàn phím (lỗi A12).
   Vì sao phải viết riêng thay vì để luật chung của theme.css lo: luật đó vẽ vòng focus NGOÀI phần tử
   2px, mà hộp dropdown có overflow-hidden nên vòng bị cắt cụt hai bên trái/phải (mục rộng đúng bằng
   hộp). Đẩy vào trong -2px thì vòng nằm trọn bên trong.
   Viết tay trong file này nên KHÔNG cần chạy npm run build:css. */
.vp-option-dropdown:focus-visible {
    outline-offset: -2px;
    background-color: var(--stone-100);
}

/* =====================================================================================
   ÁO MỚI 25/08/2026 — phần riêng của trang Ôn Luyện (lớp đè chung nằm CUỐI theme.css).
   File này nạp SAU theme.css nên mấy giá trị viết cứng ở trên đã được đổi thẳng về token
   áo mới; hai luật dưới đây là chỗ theme.css KHÔNG với tới được vì phần tử thiếu class
   rounded-sm hoặc mang nhầm "vai" thẻ HTML.
   ===================================================================================== */

/* Nút mở dropdown Cấp Học / Môn Học: nhìn và dùng như Ô NHẬP (có nhãn nổi đè lên viền)
   nên bo 11px như .o-nhap, KHÔNG viên thuốc — viên thuốc thì cung góc (~25px) nuốt mất
   chỗ nhãn "Cấp Học" đang đứng ở left 14px. */
#nut-dropdown-cap, #nut-dropdown-mon { border-radius: 11px; }

/* Thẻ chọn môn / chọn năm ở tab Kho Past Papers: hình dáng là THẺ nhưng thẻ HTML là <button>,
   nên luật "button.rounded-sm = viên thuốc" của theme.css không dùng được — bo 14px thẳng ở
   đây. Trong trang chỉ hai lưới chọn đó có <button> làm con trực tiếp của .grid. */
div.grid > button.bg-white.border { border-radius: var(--ao-r-nho); }

/* ---- ĐỢT 1 MỤC 1.3 (bổ sung 28/08/2026): nhấc hai lưới chọn ĐÚNG CÁCH ----
   Hai thẻ này trước đây nhấc lên bằng lớp tiện ích Tailwind hover:-translate-y-0.5 viết thẳng
   trong practice.html. Đó đúng là loại lỗi mục 1.3 đang đi chữa: lớp tiện ích không có cách nào
   bọc vào @media (hover: hover), nên trên điện thoại trạng thái :hover dính lại sau cú chạm và
   thẻ bị nâng lên rồi TREO ở đó cho tới khi người dùng chạm chỗ khác.
   Đã gỡ lớp đó khỏi HTML, việc nhấc chuyển về đây. Biên độ đổi từ -0.125rem (2px, con số của
   NÚT) sang -3px cho khớp hai con số cứng của hệ: nhấc THẺ là 3px, nhấc NÚT là 2px. Hai lưới
   này tuy dùng thẻ <button> nhưng hình dáng và vai trò là THẺ.
   Phần đổi viền và đổi bóng vẫn ở lại HTML (hover:border-navy-500 hover:shadow-lg), đúng luật
   "chỉ chắn phần transform, để phần màu/viền ra ngoài": máy cảm ứng vẫn cần một phản hồi.
   transition đã có sẵn: lớp .transition của Tailwind trên chính hai thẻ đó phủ cả transform. */
@media (hover: hover) and (pointer: fine) {
    div.grid > button.bg-white.border:hover { transform: translateY(-3px); }
}

/* ---- THẺ GÓI (.goi-the) ĐÃ CHUYỂN SANG assets/css/theme.css (31/08/2026) ----
   Lý do: từ hôm nay bảng giá gói Topical có BẢN THỨ HAI, trong modal "Mở khoá làm bài trên đề"
   của paper-viewer.html. Trang đó không nạp practice.css nên nhãn "Được nhiều học sinh tin dùng
   nhất" rơi xuống dòng thường và đội tiêu đề gói xuống 36px, đo được bằng getBoundingClientRect.
   Chép khối này sang file thứ hai là nhân bản đúng thứ dự án đang mắc ở màn mời mua, nên đưa hẳn
   lên theme.css, file mà CẢ HAI trang đều nạp và nạp TRƯỚC practice.css. Không đổi một dòng
   luật nào, chỉ đổi chỗ ở. */

/* ---- Danh sách câu hỏi ĐI THEO MÀN HÌNH khi cuộn (26/08/2026, yêu cầu chủ dự án) ----
   Trước đây cột danh sách là một khối tĩnh nằm cạnh khung xem câu: cuộn xuống đọc tới giữa một câu
   9618 dài ba màn hình là cả cột danh sách đã trôi hết lên trên, muốn nhảy sang câu khác phải cuộn
   ngược lên đầu trang. Hộp cuộn max-h-[70vh] bên trong KHÔNG cứu được chuyện đó — nó chỉ giới hạn
   chiều cao của danh sách, còn bản thân cột vẫn trôi theo trang.

   top: 5rem: giống hệt .thanh-cong-cu-cau ở trên — header trang sticky top-0 cao h-20 = 80px ở mọi
   bề rộng. Để top: 0 thì đầu danh sách chui xuống dưới header.

   Vì sao position: sticky chạy được ở đây: hàng bao ngoài là flex với items-start, nên cột này KHÔNG
   bị kéo cao bằng khung xem câu (align-items mặc định là stretch sẽ làm thế, và một phần tử cao bằng
   đúng khung cha thì không còn chỗ nào để dính). Đừng bỏ items-start ở div bao ngoài.

   Chỉ từ 1024px: hẹp hơn thì danh sách nằm TRÊN khung xem theo chiều dọc (flex-col), ghim nó lại là
   nó che mất chính phần câu hỏi đang đọc.

   max-height + flex cột: cột phải tự cắt cho vừa phần màn hình còn lại dưới header, rồi nhường toàn
   bộ chỗ thừa cho hộp cuộn danh sách. 1rem cuối là khoảng thở dưới đáy màn hình.
   Hàng "Thu Gọn / Bỏ Chọn" giữ nguyên chiều cao thật (flex-shrink: 0) nên luôn nhìn thấy. */
@media (min-width: 1024px) {
    .cot-cau-hoi {
        position: sticky;
        top: 5rem;
        max-height: calc(100vh - 6rem);
        display: flex;
        flex-direction: column;
    }

    /* Bỏ trần 70vh của lớp Tailwind: trong cột đã có max-height, hộp cuộn phải lấy ĐÚNG phần còn
       lại chứ không phải một con số riêng. min-height: 0 là bắt buộc — thiếu nó thì phần tử flex
       không co xuống dưới kích thước nội dung và thanh cuộn không bao giờ xuất hiện. */
    .cot-cau-hoi__ds {
        max-height: none;
        flex: 1 1 auto;
        min-height: 0;
    }
}


/* =====================================================================================
   GÓI CHUYỂN ĐỘNG 28/08/2026, phần riêng của trang Ôn Luyện.
   Kế hoạch đầy đủ kèm lý do từng lựa chọn: sandbox/ke-hoach-chuyen-dong.md
   Thành phần dùng chung (.vp-khung-cho, bảng token nhịp) nằm ở assets/css/theme.css.
   ===================================================================================== */

/* ---- ĐỢT 4 MỤC 4.1: chiều cao khung chờ của khung xem câu ----------------------------
   Khung chờ phải trùng khít bố cục thật, mà bố cục thật ở đây là một trang đề Cambridge cao
   gần cả màn hình. Viết ở đây chứ không dùng lớp Tailwind vì bản build sẵn assets/css/tailwind.ea465e5f.css
   chỉ có tới .h-40 (10rem), mà thêm .h-96 vào build là bắt cả dự án chạy lại npm run build:css
   chỉ vì một con số. Cột danh sách bên trái vẫn dùng .h-24 và .h-6 đã có sẵn trong bản build. */
.kc-khung-xem {
    height: 26rem;
}

/* ---- ĐỢT 2 MỤC 2.1: ĐÃ GỠ NGÀY 28/08/2026, ngay trong ngày đưa ra ---------------------
   Bản đầu của mục này làm hai việc: ép khung ảnh cao tối thiểu 260px với nền kem để giữ chỗ, và
   cho ảnh mờ dần từ opacity 0 khi tải xong. Chủ dự án báo ngay hai triệu chứng, và cả hai đều do
   đúng đoạn đó gây ra:
     "nháy khi chuyển câu hỏi"          <- ảnh bắt đầu ở opacity 0 rồi mới mờ vào
     "giữa các câu có khoảng trống"     <- khung 260px trong khi ảnh thật thấp hơn nhiều

   ĐÃ ĐO TRÊN TRANG THẬT trước khi gỡ, chứ không đoán: một ảnh đề mẫu có kích thước gốc
   1779 x 180 pixel. Ở bề ngang khung khoảng 1100px thì nó chỉ cao khoảng 111px. Ép khung 260px
   nghĩa là chừa thừa gần 150px nền kem TRỐNG dưới mỗi ảnh, ở mọi câu. Con số 260px được chọn khi
   lập kế hoạch mà không có một phép đo nào chống lưng, và số đo thật cho thấy nó sai hẳn một bậc:
   ảnh cắt từ đề Cambridge vốn RẤT RỘNG và RẤT THẤP, không phải hình vuông.

   Cộng thêm loading="lazy", ảnh dưới màn hình chưa tải nên khung nằm đó rỗng 260px, và người dùng
   cuộn tới đâu thấy ô kem trống tới đó.

   BÀI HỌC, đừng lặp lại: giữ chỗ cho ảnh chỉ đúng khi biết TỈ LỆ THẬT của ảnh. Không biết tỉ lệ
   mà vẫn giữ chỗ thì đang đổi một lỗi nhìn thấy được (bố cục nhảy) lấy một lỗi nhìn thấy được
   khác và còn khó chịu hơn (ô trống thường trực). Cách sửa gốc vẫn là mục 2.2 trong
   sandbox/ke-hoach-chuyen-dong.md: thêm cột chiều rộng/chiều cao vào bảng topical_question_images
   rồi phát ra width/height thật. Lúc đó trình duyệt tự chừa đúng tỉ lệ, CLS về 0, và KHÔNG có ô
   trống nào cả. Trước khi có số đo thật thì không giữ chỗ còn hơn giữ sai.

   Giữ lại của mục 2.1: loading="lazy" và decoding="async" trên các ảnh trong DANH SÁCH (thuần lợi,
   không có mặt trái), và thứ tự thuộc tính đã sửa cho lazy thật sự có hiệu lực. */
/* Luật nền của .khung-anh-de (bo góc, cắt phần tràn, neo lớp phủ) đã dời sang
   assets/css/viet-bai.css ngày 30/08/2026 để trang Xem Đề dùng chung một bản. */

/* ============================================================================
   ĐỔI CÂU CHO MƯỢT (28/08/2026)
   ============================================================================
   Gốc của cú giật đã chữa trong loadTopicalImage: ảnh cũ nay ở lại tới khi ảnh mới về, nên khung
   không còn tụt về vòng xoay 80px giữa hai câu. Phần dưới đây là lớp hoàn thiện cho êm mắt.

   DÙNG animation CHỨ KHÔNG DÙNG transition. Bài học sáng nay ở trang chủ: transition trên
   transform đánh nhau với hiệu ứng rê chuột, còn animation thì chạy độc lập, và quan trọng hơn là
   nó tự chạy lại khi phần tử được dựng lại theo :key.

   KHÔNG làm ảnh trượt lên xuống: ảnh đề Cambridge rất rộng và rất thấp (đo thật: 1779x180), dịch
   dọc trên một dải ngang như thế trông như hình bị rung. Chỉ hiện dần độ mờ, KHÔNG dịch một pixel
   nào — bản đầu còn chừa lại 4px "cho có hướng" và 29/08 đã bỏ nốt, lý do ở ngay khối dưới.

   prefers-reduced-motion đã có luật chung phủ toàn site trong theme.css (đặt mọi thời lượng về
   .01ms thay vì none, để các mốc kết thúc hiệu ứng vẫn bắn), nên ở đây không lặp lại. */
/* backwards, KHÔNG both (14/09/2026). Mốc cuối là opacity 1, trùng trạng thái thường, nên giữ mốc cuối sau khi chạy
   xong không đổi gì trên màn hình; nhưng một hiệu ứng opacity còn giữ fill thì trình duyệt lập ngữ cảnh xếp lớp cho
   khối này SUỐT ĐỜI nó. Khi đó lớp phủ viết bài của mọi dải ảnh bị nhốt trong khối, và khung chọn nét thò xuống tờ
   giấy làm bài (.vb-lop--giay, đứng sau khối) thua tờ giấy: bấm góc co giãn là rơi xuống tờ giấy và bỏ chọn luôn
   (soát phản biện tái hiện được, gỡ fill là bấm trúng). backwards vẫn giữ mốc .45 lúc bắt đầu. Riêng .dang-doi-cau
   bên dưới vẫn phải both vì nó cần ĐỨNG LẠI ở .45. */
.khoi-anh-cau {
    animation: anh-cau-hien var(--ao-vua) var(--ao-ease-vao) backwards;
}

/* SỬA 29/08/2026, sau khi chủ dự án vẫn báo "giật giật" ngay hôm sau bản 28/08 lên production:
   1. BỎ HẲN `translate: 0 4px`. Chính chú thích bên trên đã lập luận đúng rằng ảnh đề Cambridge
      rất rộng và rất thấp (1779x180) nên dịch dọc trông như hình bị rung — rồi vẫn để lại 4px.
      Bốn pixel đó chạy lại ở MỌI lượt đổi câu, và trên một dải ngang thấp thì mắt đọc nó đúng là
      một cú nảy. Đây cũng chính là thứ đã bị gỡ một lần ngày 28/08 (xem chú thích .khung-anh-de
      phía trên: "nháy khi chuyển câu hỏi"), đừng dựng lại lần thứ ba.
   2. Bắt đầu từ .45 chứ KHÔNG từ 0. Ảnh đang chờ đã mờ xuống .45 bằng anh-cau-mo-cho ngay bên
      dưới; cho nhịp hiện dần chạy lại từ 0 nghĩa là màn hình đi qua trạng thái TRỐNG HẲN giữa hai
      ảnh — đúng một cú chớp, ngay giữa hiệu ứng sinh ra để khỏi phải thấy khung trống. Nối liền
      hai mốc .45 thì ảnh chỉ sáng dần lên, không có khoảng đứt nào. */
@keyframes anh-cau-hien {
    from { opacity: .45; }
    to   { opacity: 1; }
}

/* Ảnh mới đang trên đường về: làm nhạt ảnh đang đọc để báo "máy đang đổi câu", nhưng vẫn giữ
   nguyên chiều cao nên không có gì nhảy. Chậm hơn nhịp hiện dần một chút cho khỏi chớp khi mạng
   nhanh: tải xong trong vòng 90ms thì gần như không kịp thấy mờ. */
.khoi-anh-cau.dang-doi-cau {
    animation: anh-cau-mo-cho var(--ao-cham) var(--ao-ease-vao) both;
}

@keyframes anh-cau-mo-cho {
    from { opacity: 1; }
    to   { opacity: .45; }
}

/* ---- ĐỢT 2 MỤC 2.5: vạch dọc báo thẻ câu ĐÃ CHỌN vào worksheet ------------------------
   Nền gold-50 và viền gold-500 do lớp Tailwind trong practice.html lo (cả hai lớp ĐÃ có sẵn
   trong bản build, không cần npm run build:css). Ở đây chỉ thêm vạch dọc 3px, vì trạng thái
   KHÔNG được báo chỉ bằng màu: người mù màu vàng/xám không phân biệt nổi gold-50 với trắng.
   Thứ tự ưu tiên giữa ba trạng thái chồng nhau ghi ở ngay chỗ sửa trong practice.html. */
.cau-the--da-chon {
    position: relative;
}

.cau-the--da-chon::before {
    content: '';
    position: absolute;
    /* Bám sát mép TRONG của viền (top/bottom/left = 0 là mép padding box), KHÔNG dùng -1px để
       trùm ra ngoài: thẻ nằm trong hộp cuộn .cot-cau-hoi__ds có overflow:auto, mà phần tràn ra
       phía trái và phía trên của một hộp cuộn thì bị cắt cụt chứ không cuộn tới được. */
    top: 0;
    bottom: 0;
    left: 0;
    width: 3px;
    background: var(--gold-500, #C08A2E);
    border-radius: 2px 0 0 2px;
    pointer-events: none;
}

/* ---- ĐỢT 4 MỤC 4.3: nhịp chấm câu ----------------------------------------------------
   Đây là NGOẠI LỆ DUY NHẤT của ranh giới "bàn học phải im" trong kế hoạch: học sinh vừa bấm Nộp
   Bài và đang chờ đúng khoảnh khắc này, nên chuyển động ở đây trả lời một câu hỏi có thật.

   Bốn điều kiện cứng của hạng mục, đều thoả:
   1. Rung tối đa 4px, chạy ĐÚNG MỘT lượt, không lặp (không có `infinite` ở đâu bên dưới).
   2. Phần RUNG bọc trong @media (prefers-reduced-motion: no-preference). Người bật giảm chuyển
      động chỉ còn viền đỏ tĩnh (lớp border-red-200 của Tailwind), không rung.
   3. Đúng/sai KHÔNG báo bằng màu và chuyển động: icon fa-circle-check / fa-circle-xmark cộng
      dòng chữ "Bạn chọn / Đáp án đúng" ở practice.html mới là thông tin thật.
   4. Không chép particle burst hay chuỗi ngày học: đó là cơ chế thưởng phạt, không còn là
      phản hồi, và không phải giọng của một tổ chức khảo thí.

   Vì sao dùng animation chứ không transition: khối kết quả dựng mới bằng v-if khi chấm xong, nên
   animation tự chạy đúng một lượt lúc gắn vào DOM, không cần JS bật rồi tắt.
   --xanh-nen viết kèm giá trị dự phòng: biến đó hiện chỉ khai trong .tc ở trang-chu.css, chưa lên
   :root chung, nên ở trang này var() sẽ rơi về #E9F7F1. */
.nhip-cham--dung {
    animation: vp-cham-dung 380ms ease-out 1;
}

@keyframes vp-cham-dung {
    from { background-color: var(--xanh-nen, #E9F7F1) }
    to   { background-color: var(--trang, #FFFFFF) }
}

@media (prefers-reduced-motion: no-preference) {
    .nhip-cham--sai {
        animation: vp-cham-sai 240ms ease-out 1;
    }

    /* Biên độ lớn nhất đúng 4px, và tắt dần về 0 để cú rung kết thúc êm chứ không dừng đột ngột. */
    @keyframes vp-cham-sai {
        0%   { transform: translateX(0) }
        20%  { transform: translateX(-4px) }
        45%  { transform: translateX(3px) }
        70%  { transform: translateX(-2px) }
        100% { transform: translateX(0) }
    }
}


/* =====================================================================================
   MƯỢT HOÁ TAB / COMPONENT / ĐỔI CÂU — 29/08/2026, theo lời chủ dự án:
   "ấn vào giữa các component ở trên mục topical question past paper, cái khung chưa load
    nên nó trông trống"; "cứ cảm giác không được mượt, với giật giật"; "ấn giữa các câu
    hỏi cũng giật giật".
   Ba nguyên nhân đã soi ra, và cả ba đều là lỗi BỐ CỤC chứ không phải thiếu hiệu ứng:
     a) khung chờ thấp hơn hẳn nội dung thật -> trang sập xuống rồi bung ra, hai cú nhảy
        cho mỗi cú bấm;
     b) ảnh đề không có width/height nên trình duyệt không chừa được chỗ, tấm sau về là
        đẩy tấm trước đi;
     c) mọi lượt đổi câu đều tải lại ảnh từ đầu, kể cả câu vừa xem xong một giây trước.
   Phần (b) và (c) chữa trong assets/js/pages/practice.page.js (kho ảnh + nạp trước +
   doKichThuocAnh). Phần (a) chữa bằng hai luật dưới đây cộng hai :style trong practice.html.
   ===================================================================================== */

/* Chiều cao DỰ PHÒNG của khung chờ ảnh đề, chỉ dùng ở lượt mở trang đầu tiên khi chưa có
   khung nào để đo. Mọi lượt sau, practice.page.js gán thẳng height bằng số đo thật của
   khung ảnh vừa biến mất (giuChoTruocKhiTai) và luật này bị style inline đè — đúng ý đồ.
   Cùng con số 26rem với .kc-khung-xem ở trên cho hai khung chờ của cùng một cột nhìn thuận
   mắt nhau, và cùng một lý do không dùng lớp Tailwind: bản build sẵn chỉ có tới .h-40. */
.kc-anh-de {
    height: 26rem;
}

/* Phản hồi khi BẤM cho các chip lọc và ba nút tab: lún nhẹ 3% rồi bật lại.
   Vì sao cần: ba nhóm nút này đổi cả màn hình bên dưới, mà lúc mạng chậm thì sau cú bấm
   không có gì nhúc nhích trong vài trăm mili giây đầu — người dùng không biết máy đã nhận
   cú bấm chưa. Đây là phản hồi TỨC THÌ, không phụ thuộc mạng.
   Cùng khuôn với .nut:active ở cuối theme.css (translateY(0) scale(.97)), chỉ bỏ phần
   translateY vì chip không có luật nhấc lên khi rê chuột để mà trả về.
   KHÔNG khai transition ở đây: cả 11 nút đã mang sẵn lớp .transition của Tailwind, và lớp
   đó có phủ transform (đã kiểm trong assets/css/tailwind.ea465e5f.css). Khai lại chỉ tổ đè mất phần
   đổi màu 150ms đang chạy đúng.
   KHÔNG cần bọc @media (hover: hover): luật bắt buộc bọc chỉ áp cho :hover có transform —
   :hover dính lại sau cú chạm, còn :active thì trình duyệt tự nhả ngay khi nhấc ngón tay. */
.chip-loc:active:not(:disabled) {
    transform: scale(.97);
}

/* ---------------------------------------------------------------------------
   TAB VIDEO GIẢI (29/08/2026)
   Khung phát video giải bài, thay chỗ khung ảnh khi bấm tab thứ ba.

   Vì sao chặn chiều cao: video giảng bài quay bằng điện thoại có thể là khung
   DỌC (9:16). Để tự do thì một video dọc cao gấp ba màn hình, đẩy hết phần điều
   hướng câu xuống dưới và người xem phải cuộn mới thấy thanh tua. 78vh giữ trọn
   khung phát trong một màn hình ở mọi tỉ lệ; object-fit: contain nên video ngang
   vẫn vừa đủ bề ngang, không bị cắt cạnh nào.
   --------------------------------------------------------------------------- */
.khung-video-giai video{
    display:block;
    width:100%;
    max-height:78vh;
    object-fit:contain;
}

/* ---- CỬA SỔ NỔI cho tài liệu kèm câu (insert / tờ tham khảo), 02/09/2026 ----
   Cùng một khung với .khung-chia__insert, chỉ đổi lớp: position fixed để câu hỏi phía sau giữ NGUYÊN
   bố cục (đó là điểm chính của chế độ này), resize: both của CSS cho người dùng kéo góc đổi cỡ mà không
   cần một dòng JS, còn kéo di chuyển thì dùng thanh tiêu đề làm tay cầm (pointer events trong
   practice.page.js). Mặc định neo góc dưới phải; kéo rồi thì left/top do JS đặt inline đè lên. */
.khung-noi {
    position: fixed;
    right: 24px;
    bottom: 24px;
    width: min(560px, calc(100vw - 32px));
    height: min(70vh, calc(100vh - 48px));
    min-width: 280px;
    min-height: 200px;
    max-width: calc(100vw - 16px);
    max-height: calc(100vh - 16px);
    z-index: 60;
    overflow: auto;
    overscroll-behavior: contain;
    resize: both;
    background: var(--ao-nen-lanh);
    border: 1px solid var(--ao-vien);
    border-radius: var(--ao-r-nho);
    box-shadow: 0 18px 48px -12px rgba(15, 23, 42, 0.35), 0 4px 12px -4px rgba(15, 23, 42, 0.15);
}
.khung-noi__tay-cam {
    cursor: move;
    user-select: none;
    touch-action: none;
}

/* Mẩu "tab" ở góc dưới phải khi cửa sổ nổi đang ẩn tạm (popupAn). Cùng z-index với .khung-noi. */
.khung-noi__tab {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 60;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    font-size: 12px;
    font-weight: 700;
    color: #fff;
    background: var(--navy-900);
    border: 1px solid var(--ao-vien);
    border-radius: var(--ao-r-nho);
    box-shadow: 0 10px 28px -10px rgba(15, 23, 42, 0.45);
    cursor: pointer;
}
.khung-noi__tab:hover { background: var(--navy-700); }

/* =============================================================================================
   KHUNG CHẠY THỬ PYTHON (18/09/2026, sửa bố cục 19/09 theo phản hồi chủ dự án: "diện tích đang
   quá bé, cả về mặt câu hỏi và cái IDE này nữa")
   =============================================================================================
   BẢN ĐẦU SAI Ở ĐÂU: khung code nằm THẲNG HÀNG DỌC bên trên ảnh câu hỏi, trong một thẻ đã bị
   .the-xem-cau--viet giới hạn max-height. Hai hậu quả nhìn thấy trên ảnh chụp của chủ dự án:
   câu hỏi bị đẩy hẳn khỏi màn hình (đang viết code thì không đọc được đề nữa — đúng thứ Paper 4
   cần nhất), và phần vượt quá chiều cao thẻ tràn ra ngoài, ĐÈ lên hàng "Câu Trước / Câu Tiếp Theo".

   NAY DÙNG LẠI ĐÚNG KHUNG CHIA ĐÔI CỦA TỜ INSERT: câu hỏi một bên, code một bên, mỗi bên cuộn
   riêng, kéo được ranh giới, cao 76vh. Khác đúng một điểm — thứ tự đảo (row-reverse) để CÂU HỎI
   nằm bên TRÁI còn code bên PHẢI, theo thói quen đọc đề rồi gõ. Nhờ đảo bằng CSS mà DOM vẫn giữ
   thứ tự [khung tài liệu][tay kéo][câu hỏi] như cũ, không phải viết lại chỗ nào đang chạy tốt.
   Riêng phép tính lúc kéo phải lật lại (xem batDauKeoKhung trong practice.page.js). */
.khung-chia--code { flex-direction: column-reverse; }

/* Thẻ đang bật Viết Bài có luật riêng ép .vung-viet-bai__noi-dung thành cột (viet-bai.css) — nó
   cụ thể hơn .khung-chia nên sẽ thắng và làm khung chia dẹp thành hàng dọc. Hai dòng dưới đây
   cụ thể hơn nữa, để vừa viết bài vừa mở khung code vẫn ra hai cột. */
.the-xem-cau--viet .vung-viet-bai__noi-dung.khung-chia--code { flex-direction: column-reverse; }

@media (min-width: 1024px) {
    .khung-chia--code,
    .the-xem-cau--viet .vung-viet-bai__noi-dung.khung-chia--code { flex-direction: row-reverse; }

    /* TRONG THẺ ĐANG VIẾT BÀI thì BỎ chiều cao 76vh của .khung-chia và để nó co theo phần còn lại.
       Vì sao: thẻ lúc đó có max-height (100dvh - 6.5rem) và chia dọc, mà 76vh là một con số CỨNG
       không biết gì về thanh công cụ câu lẫn dải tệp phía trên nó. Đo thật ở 1440x900 trước khi vá:
       khung code thò khỏi đáy thẻ 228px và ĐÈ lên hàng "Câu Trước / Câu Tiếp Theo" — đúng chỗ hỏng
       trong ảnh chụp của chủ dự án ngày 19/09. */
    .the-xem-cau--viet .vung-viet-bai__noi-dung.khung-chia--code {
        height: auto;
        flex: 1 1 auto;
        min-height: 0;
    }
    /* Khung code dùng lại biến --rong-insert (cùng một thanh kéo, cùng một chỗ nhớ). */
    .khung-chia--code .khung-chia__cau-hoi { padding-left: 0; padding-right: 0.75rem; }
}

/* PHÓNG TO: code chiếm trọn bề ngang, câu hỏi và tay kéo tạm ẩn. Dành cho lúc chỉ ngồi gõ code —
   bấm lại là câu hỏi quay về đúng chỗ cũ, không mất gì. */
.khung-chia--code-het .khung-chia__cau-hoi,
.khung-chia--code-het .khung-chia__tay-keo { display: none; }
.khung-chia--code-het .khung-chia__insert--code { width: 100%; flex: 1 1 auto; }

/* Ruột khung code: thanh công cụ trên, ô gõ giữa (giãn hết phần còn lại), kết quả dưới. overflow
   hidden ở ngoài để ba tầng bên trong tự cuộn — khác .khung-chia__insert (overflow:auto) vốn để
   cuộn cả tờ PDF. */
.khung-chia__insert--code {
    overflow: hidden;
    display: flex;
    flex-direction: column;
    background: #fff;
}

.o-code-thanh {
    flex: none;
    display: flex;
    align-items: center;
    gap: .375rem;
    flex-wrap: wrap;
    padding: .375rem .5rem;
    border-bottom: 1px solid var(--ao-vien, #E7E5E4);
    background: #fff;
}

.o-code-hang {
    flex: 1 1 auto;
    min-height: 140px;
    display: flex;
    overflow: hidden;
}

/* CỘT SỐ DÒNG. Ba con số dưới đây phải khớp TỪNG LI với ô gõ bên cạnh (font, cỡ chữ, chiều cao
   dòng, lề trên), nếu không thì cuộn tới dòng 40 là số đã lệch hẳn một dòng so với code.
   Vì thế ô gõ để wrap="off": một dòng logic luôn là đúng một dòng hiển thị, không bao giờ có
   chuyện một dòng dài tự gập làm hai mà cột số chỉ đếm một. */
.o-code-so {
    flex: none;
    width: 3rem;
    overflow: hidden;
    padding: .5rem .5rem .5rem 0;
    text-align: right;
    background: #FAFAF9;
    border-right: 1px solid #E7E5E4;
    color: #A8A29E;
    user-select: none;
}

/* BỘ SỐ ĐO CỦA CHỮ trong ô code — khai ở CẢ BA nơi phải khớp: cột số, ô gõ, lớp màu.
   Ghi trên .o-code-so (cả cột) chứ không trên từng con số: từ lúc mỗi số dòng thành một <button>
   (để bấm đặt điểm dừng), nút mang `font: inherit` và nó thừa kế từ CỘT — mà cột thì đang là chữ
   thường 16px của trang. Hậu quả đo được ngày 19/09/2026: số dòng cách nhau 24px trong khi dòng
   code cách nhau 21,06px, tới dòng 10 là cột số trôi khỏi code gần 30px. Đổi cỡ chữ ở đây thì đổi
   cả ba dòng dưới cùng lúc, đừng đổi lẻ một chỗ. */
.o-code-so,
.o-code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
    font-size: 13px;
    line-height: 1.62;
}

/* Dòng ĐANG HỎNG: tô đỏ ngay ở cột số, cùng chỗ mắt đang nhìn khi đọc lỗi. */
.o-code-so__dong--loi {
    color: #B91C1C;
    font-weight: 700;
    background: #FEE2E2;
    border-radius: 2px;
}

.o-code {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    outline: none;
    resize: none;
    padding: .5rem .75rem;
    color: #0D1F33;
    white-space: pre;
    overflow: auto;
    tab-size: 4;
}

.o-code-nhap { flex: none; border-top: 1px solid var(--ao-vien, #E7E5E4); }

/* KHUNG KẾT QUẢ: cao CỐ ĐỊNH (chiều cao nằm ở style inline, người dùng kéo được), tự cuộn.
   Trước 19/09/2026 nó cao theo nội dung, nên một vòng lặp in 500 dòng là đẩy ô gõ code khuất hẳn —
   đúng cảnh chủ dự án gặp. flex:none để nó không bị co giãn theo phần còn lại. */
.o-code-ra {
    flex: none;
    overflow: auto;
    overscroll-behavior: contain;
    padding: .5rem .75rem .625rem;
    background: #FCFCFB;
}

/* Thanh kéo nằm ngay trên khung kết quả. Cao 6px nhưng vùng bấm nới ra hai bên bằng padding ảo của
   con trỏ; ::before là vạch nhìn thấy. */
.o-code-keo {
    flex: none;
    height: 7px;
    border-top: 1px solid var(--ao-vien, #E7E5E4);
    background: #F5F5F4;
    cursor: row-resize;
    touch-action: none;
    position: relative;
}
.o-code-keo::before {
    content: "";
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 2.25rem;
    height: 2px;
    border-radius: 2px;
    background: #D6D3D1;
}
.o-code-keo:hover::before,
.o-code-keo:focus-visible::before { background: var(--navy-500, #2C4A6E); }
.o-code-keo:focus-visible { outline: 2px solid var(--navy-500, #2C4A6E); outline-offset: -2px; }

/* ---- Thanh gỡ lỗi từng bước (19/09/2026) ---- */
/* Dòng chương trình ĐANG ĐỨNG ở bước này. Xanh chứ không đỏ: đây là "đang ở đây", không phải "hỏng
   ở đây". Dòng hỏng vẫn tô đỏ như cũ, và khi hai cái trùng nhau thì màu xanh thắng vì lúc đó người
   ta đang tua chứ không đang đọc lỗi. */
.o-code-so__dong--dang-o {
    color: #fff;
    font-weight: 700;
    background: var(--navy-600, #1F3A59);
    border-radius: 2px;
}

.o-code-go {
    flex: none;
    border-top: 1px solid var(--ao-vien, #E7E5E4);
    background: #F8FAFC;
    padding: .375rem .5rem .5rem;
    display: flex;
    flex-direction: column;
    gap: .375rem;
    max-height: 40%;
    overflow: auto;
    overscroll-behavior: contain;
}

.o-code-go__nut {
    padding: .25rem .5rem;
    border-radius: 2px;
    font-size: 11px;
    font-weight: 700;
    color: #44403C;
    background: #fff;
    border: 1px solid #E7E5E4;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
}
.o-code-go__nut:hover:not(:disabled) { background: #F5F5F4; }
.o-code-go__nut:disabled { opacity: .4; cursor: not-allowed; }
.o-code-go__nut--chinh { background: var(--navy-600, #1F3A59); color: #fff; border-color: var(--navy-600, #1F3A59); }
.o-code-go__nut--chinh:hover:not(:disabled) { background: var(--navy-700, #16324F); }

.o-code-go__thanh { width: 100%; accent-color: var(--navy-600, #1F3A59); }

/* 13rem chứ không phải 9rem (nới 19/09/2026): một bài có hàm là chồng gọi đã hai khung, mà 9rem
   chỉ đủ cho khung ngoài cộng một hàng rưỡi của khung trong — nhìn ra như bảng bị cắt cụt chứ
   không ra như bảng cuộn được. Khối .o-code-go bọc ngoài vẫn chặn ở 40% chiều cao khung code nên
   nới ở đây không đẩy ô gõ code hẹp đi. */
.o-code-go__bang {
    background: #fff;
    border: 1px solid #E7E5E4;
    border-radius: 2px;
    padding: .25rem .5rem;
    max-height: 13rem;
    overflow: auto;
}

/* ---- Bộ soạn kiểu IDE: nút thanh công cụ, điểm dừng, bảng gợi ý, báo cú pháp (19/09/2026) ---- */
.o-code-thanh__nut {
    padding: .3125rem .5rem;
    border-radius: 2px;
    font-size: 11px;
    font-weight: 700;
    color: #44403C;
    background: #F5F5F4;
    border: 0;
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    white-space: nowrap;
}
.o-code-thanh__nut:hover:not(:disabled) { background: #E7E5E4; }
.o-code-thanh__nut:disabled { opacity: .45; cursor: not-allowed; }
.o-code-thanh__nut--chinh { background: var(--navy-600, #1F3A59); color: #fff; }
.o-code-thanh__nut--chinh:hover { background: var(--navy-700, #16324F); }
.o-code-thanh__nut--dung { background: #DC2626; color: #fff; }
.o-code-thanh__nut--go { background: var(--gold-100, #F5E4BB); color: var(--gold-700, #8A5F19); }
.o-code-thanh__nut--bat { background: var(--navy-100, #DCE4EC); color: var(--navy-800, #122840); }

.o-code-hang { position: relative; }

/* Số dòng nay là NÚT (bấm để đặt điểm dừng) nên phải gỡ hết áo mặc định của button và giữ đúng
   chiều cao dòng — lệch một pixel là cả cột trượt khỏi code. */
.o-code-so__dong {
    display: block;
    width: 100%;
    padding: 0;
    border: 0;
    background: transparent;
    text-align: right;
    color: inherit;
    font: inherit;
    cursor: pointer;
}
.o-code-so__dong:hover { background: #E7E5E4; }

/* ĐIỂM DỪNG: chấm đỏ nằm bên trái con số, đúng chỗ mọi IDE vẫn đặt. */
.o-code-so__dong--dung {
    position: relative;
    color: #B91C1C;
    font-weight: 700;
}
.o-code-so__dong--dung::before {
    content: "";
    position: absolute;
    left: .125rem;
    top: 50%;
    transform: translateY(-50%);
    width: .4375rem;
    height: .4375rem;
    border-radius: 50%;
    background: #DC2626;
}

.o-code-goiy {
    position: absolute;
    z-index: 30;
    margin: 0;
    padding: .125rem;
    list-style: none;
    min-width: 9rem;
    max-height: 11rem;
    overflow: auto;
    background: #fff;
    border: 1px solid #D6D3D1;
    border-radius: 3px;
    box-shadow: 0 6px 18px rgba(13, 31, 51, .14);
}
.o-code-goiy__muc {
    display: block;
    width: 100%;
    text-align: left;
    padding: .1875rem .375rem;
    border: 0;
    border-radius: 2px;
    background: transparent;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 11px;
    color: #1C1917;
}
.o-code-goiy__muc--chon,
.o-code-goiy__muc:hover { background: var(--navy-600, #1F3A59); color: #fff; }

.o-code-cuphap {
    flex: none;
    display: flex;
    align-items: center;
    gap: .375rem;
    width: 100%;
    text-align: left;
    padding: .3125rem .625rem;
    border: 0;
    border-top: 1px solid #FECACA;
    background: #FEF2F2;
    color: #B91C1C;
    font-size: 11px;
    font-weight: 700;
}
.o-code-cuphap:hover { background: #FEE2E2; }

/* Hai bảng biến nằm cạnh nhau từ 640px, xếp dọc khi hẹp. */
/* flex: none cho MỌI con của .o-code-go.
   .o-code-go là flex cột có max-height 40%; con mặc định mang flex-shrink: 1, nên khi tổng chiều
   cao vượt trần thì trình duyệt BÓP chúng lại thay vì cuộn. Bắt được ngày 19/09/2026 khi nới bảng
   chồng gọi lên 13rem: hộp "Evaluating" tụt từ 31px xuống 12px, chữ trong đó bị cắt đúng nửa trên
   và nhìn như một vệt kẻ. Panel đã có overflow: auto rồi, việc đúng là cuộn chứ không phải bóp. */
.o-code-go__cot { display: grid; grid-template-columns: 1fr; gap: .375rem; flex: none; }
.o-code-go > * { flex: none; }
@media (min-width: 640px) {
    .o-code-go__cot { grid-template-columns: 1fr 1fr; }
}
/* Biến vừa đổi giá trị so với bước trước. */
.o-code-go__vua-doi { background: #FEF9C3; }

/* Khối traceback: giữ nguyên chữ của Python, nhưng dòng chỉ vào code học sinh thì bấm được để nhảy
   tới đúng dòng đó (lệnh chủ dự án 19/09/2026). Nút phải là `display:inline` và bỏ sạch áo mặc định
   của <button>, nếu không nó tự xuống dòng và phá mất cách trình bày của traceback. */
.o-code-loi {
    margin: 0;
    padding: .5rem .625rem;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 11px;
    line-height: 1.6;
    color: #991B1B;
    background: #FEF2F2;
    border: 1px solid #FECACA;
    border-radius: 3px;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.o-code-loi__toi {
    display: inline;
    padding: 0;
    margin: 0;
    border: 0;
    background: transparent;
    font: inherit;
    color: #B91C1C;
    text-align: left;
    text-decoration: underline;
    text-underline-offset: 2px;
    cursor: pointer;
}
.o-code-loi__toi:hover { background: #FEE2E2; color: #7F1D1D; }

/* ---- TÔ MÀU CÚ PHÁP: hai lớp chồng khít (19/09/2026) ----
   Lớp dưới là <pre> đã tô màu, lớp trên là textarea trong suốt. MỌI số đo ảnh hưởng tới vị trí chữ
   phải giống nhau từng li ở cả hai: font, cỡ chữ, chiều cao dòng, lề, cách xuống dòng, cỡ tab.
   Lệch một pixel là nhìn thấy ngay hai bản chữ chồng lệch nhau. */
.o-code-boc {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
}

.o-code-mau {
    position: absolute;
    inset: 0;
    margin: 0;
    padding: .5rem .75rem;
    overflow: hidden;
    white-space: pre;
    tab-size: 4;
    pointer-events: none;
    color: #0D1F33;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
    font-size: 13px;
    line-height: 1.62;
}
.o-code-mau code { font: inherit; }

/* Chữ của textarea trong suốt, chỉ còn con trỏ và vùng bôi đen là thấy — màu sắc do lớp dưới lo. */
.o-code {
    position: relative;
    background: transparent;
    color: transparent;
    caret-color: #0D1F33;
}
/* NỀN VÙNG BÔI ĐEN PHẢI TRONG SUỐT MỘT PHẦN.
   Chữ của textarea là màu trong suốt (màu thật do lớp <pre> bên dưới vẽ), mà vùng bôi đen thì
   trình duyệt tô ở CHÍNH lớp textarea, tức là NẰM TRÊN lớp màu. Nền đục (#C7D2FE) vì thế che sạch
   chữ bên dưới: bôi đen tới đâu là chữ biến mất tới đó, còn trơ một khối xanh. Chủ dự án gặp đúng
   cảnh này ngày 19/09/2026. Nền 22% thì vừa thấy rõ vùng đang chọn, vừa đọc được chữ xuyên qua.
   Lớp <pre> cũng phải cấm chọn chữ: nếu không, kéo chuột qua cả trang sẽ bôi đen CẢ HAI lớp và chữ
   chồng nhau hai lần. */
.o-code::selection { background: rgba(37, 99, 235, .22); }
.o-code::-moz-selection { background: rgba(37, 99, 235, .22); }
.o-code-mau { user-select: none; -webkit-user-select: none; }

/* BẢNG MÀU ĐỔI 19/09/2026 — chủ dự án: "bảng màu của bạn quá khó nhìn". Bản đầu lấy thẳng tông
   navy/gold của trang nên hai lỗi cùng lúc: từ khoá (#1F3A59) và hàm sẵn (#2C4A6E) gần như một màu,
   tên hàm (#8A5F19) và số (#A6741F) cũng vậy — sáu loại token mà mắt chỉ phân biệt được ba. Chú
   thích #A8A29E thì độ tương phản trên nền trắng chỉ khoảng 2,2:1, dưới xa mức đọc được.
   Nay sáu SẮC KHÁC HẲN NHAU, mọi màu đều từ 4,5:1 trở lên trên nền trắng (mức AA cho chữ thường):
   xanh dương, tím, cam đất, xanh lá, đỏ mận, xám xanh. Tông trang không áp vào đây có chủ ý: đây là
   ô code, nơi việc phân biệt được token quan trọng hơn việc hợp tông. */
.ma-tu-khoa { color: #1D4ED8; font-weight: 700; }    /* for, if, def, return...  ~8:1 */
.ma-ham     { color: #7E22CE; }                       /* print, open, len...      ~7:1 */
.ma-ten     { color: #B45309; font-weight: 700; }     /* tên hàm/lớp học sinh đặt ~5:1 */
.ma-chuoi   { color: #15803D; }                       /* "chuỗi"                  ~5:1 */
.ma-so      { color: #9D174D; }                       /* 10, 2.5                  ~7:1 */
.ma-chu     { color: #64748B; font-style: italic; }   /* # chú thích              ~4,8:1 */

/* Một khung trong chồng gọi. Khung trong cùng (chỗ chương trình đang đứng) nổi lên bằng viền trái. */
.o-code-go__khung { padding: .125rem 0 .25rem; border-left: 2px solid transparent; padding-left: .375rem; }
.o-code-go__khung--trong { border-left-color: var(--navy-600, #1F3A59); background: #F8FAFC; }

/* ---- Dòng đang tính, chế độ biểu thức (19/09/2026) ---- */
.o-code-bt {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    padding: .3125rem .5rem;
    background: #fff;
    border: 1px solid var(--ao-vien, #E7E5E4);
    border-radius: 3px;
    overflow-x: auto;
}
.o-code-bt__nhan {
    flex: none;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: #A8A29E;
}
.o-code-bt code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 13px;
    line-height: 1.6;
    color: #0D1F33;
    white-space: pre;
}
/* Vừa tính xong: xanh lá đậm, nổi nhất — mắt phải rơi vào đây trước. */
.o-code-bt__vua-tinh { background: #BBF7D0; color: #14532D; font-weight: 700; border-radius: 2px; padding: 0 2px; }
/* Đã tính từ trước trong cùng câu lệnh: xanh nhạt, chỉ để nhớ nó từng là một biểu thức. */
.o-code-bt__da-tinh { background: #F0FDF4; color: #166534; border-radius: 2px; padding: 0 2px; }
/* Sắp tính: vàng, đúng màu Thonny dùng cho chỗ con trỏ chương trình sắp đụng tới. */
.o-code-bt__sap-tinh { background: #FEF08A; color: #713F12; border-radius: 2px; padding: 0 2px; }

/* ==========================================================================
   NỐT PHẦN CÒN THIẾU SO VỚI THONNY (19/09/2026)
   Bảng biến sau lượt chạy, chế độ tham chiếu, phạm vi biến, ô shell.
   ========================================================================== */

/* ---- Phạm vi biến: "Explains scopes" ----
   Biến ĐỊA PHƯƠNG in nghiêng nhẹ, không đổi màu. Lý do: sáu màu cú pháp đã dùng hết dải màu đọc
   được trên nền trắng, thêm màu thứ bảy là hai loại token bắt đầu giống nhau. Chữ nghiêng là một
   chiều KHÁC của mắt nên không tranh chỗ với màu, mà vẫn phân biệt được ngay.
   Chỗ dùng cùng MỘT biến trong CÙNG phạm vi thì có gạch chân chấm cộng nền mờ — đủ thấy, không đủ
   để át chữ. Nền phải rất nhạt vì lớp này nằm DƯỚI textarea, đậm lên là nuốt cả con trỏ. */
.ma-dp { font-style: italic; }
.ma-cung {
    background: rgba(250, 204, 21, .28);
    border-radius: 2px;
    box-shadow: 0 1px 0 rgba(161, 98, 7, .55);
}

/* Câu giải thích phạm vi, ngay dưới ô gõ. Cùng chỗ với câu báo sai cú pháp và loại trừ nhau với
   nó: sai cú pháp thì chưa phân tích được phạm vi, mà cũng chẳng ai quan tâm lúc đó. */
.o-code-pv {
    flex: none;
    display: flex;
    align-items: center;
    gap: .375rem;
    padding: .25rem .75rem;
    font-size: 11px;
    color: #57534E;
    background: #FAFAF9;
    border-top: 1px solid var(--ao-vien, #E7E5E4);
}
.o-code-pv i { color: #CA8A04; }
/* Lượt dọn dẹp có ĐOÁN (mở Tab thành dấu cách) thì nền hổ phách: đây là lần duy nhất nút này chọn
   một trong hai cách đọc mà Python từ chối chọn, nên phải nhìn khác hẳn lượt dọn dẹp thường. */
.o-code-pv--tab { background: #FFFBEB; color: #78350F; border-top-color: #FDE68A; }
.o-code-pv--tab i { color: #B45309; }

/* ---- Bảng biến sau lượt chạy ---- */
.o-code-bien {
    flex: none;
    max-height: 200px;
    overflow: auto;
    padding: .5rem .75rem .625rem;
    border-top: 1px solid var(--ao-vien, #E7E5E4);
    background: #fff;
}
.o-code-bien__nut {
    display: inline-flex;
    align-items: center;
    gap: .25rem;
    padding: .125rem .375rem;
    border: 1px solid var(--ao-vien, #E7E5E4);
    border-radius: 3px;
    background: #fff;
    font-size: 10px;
    font-weight: 700;
    color: #57534E;
    transition: background .15s, color .15s;
}
.o-code-bien__nut:hover:not(:disabled) { background: #F5F5F4; }
.o-code-bien__nut:disabled { opacity: .4; cursor: not-allowed; }
.o-code-bien__nut--bat { background: var(--navy-600, #1F3A59); border-color: var(--navy-600, #1F3A59); color: #fff; }

/* Địa chỉ đối tượng trong chế độ tham chiếu. Số hiệu ngắn (#1, #2) chứ không phải id() thật của
   Python: con số 9 chữ số chiếm hết bảng mà nghĩa duy nhất cần truyền là "cùng số là cùng một vật". */
.o-code-id {
    display: inline-block;
    padding: 0 .25rem;
    border-radius: 2px;
    background: #F1F5F9;
    color: #64748B;
    font-size: 10px;
    font-weight: 700;
}
.o-code-id--chung { background: #DDD6FE; color: #5B21B6; }

/* ==========================================================================
   MỘT CONSOLE DUY NHẤT: OUTPUT + INPUT + SHELL (19/09/2026)
   Thay cho ba khung rời trước đó (Output chỉ đọc, ô "Dữ liệu nhập sẵn", ô Shell).
   ========================================================================== */
.o-code-console {
    flex: none;
    display: flex;
    flex-direction: column;
    min-height: 0;
    border-top: 1px solid var(--ao-vien, #E7E5E4);
    background: #fff;
}
.o-code-console__dau {
    flex: none;
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .375rem .75rem .25rem;
}
/* Lúc chương trình đang ngủ chờ một dòng: nhãn hổ phách nhấp nháy nhẹ. Đây là trạng thái DUY NHẤT
   mà người dùng bắt buộc phải làm gì đó, nên nó phải tự nhận ra được mà không cần đọc chữ. */
.o-code-console__doi {
    display: inline-flex;
    align-items: center;
    gap: .375rem;
    padding: .0625rem .375rem;
    border-radius: 999px;
    background: #FEF3C7;
    color: #92400E;
    font-size: 10px;
    font-weight: 700;
    animation: o-code-doi-nhap 1.6s ease-in-out infinite;
}
@keyframes o-code-doi-nhap { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
@media (prefers-reduced-motion: reduce) { .o-code-console__doi { animation: none; } }

.o-code-console__cuon {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    padding: 0 .75rem .5rem;
    overscroll-behavior: contain;
}
/* Mọi mẩu dùng CHUNG một bộ số đo để chúng nối nhau thành một dòng chảy liền mạch, không ra vẻ
   mấy khối rời xếp cạnh nhau. margin 0 là bắt buộc: <pre> mặc định có margin trên dưới. */
.o-code-console__cuon pre {
    margin: 0;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
    font-size: 12px;
    line-height: 1.6;
    white-space: pre-wrap;
    word-break: break-word;
}
.o-code-console__ra { color: #1C1917; }
/* Dòng mình gõ trả lời chương trình: xanh dương và đậm, để phân biệt ngay với chữ chương trình in
   ra. Trong terminal thật thì chữ gõ vào và chữ in ra cùng màu, nhưng ở đây học sinh đang HỌC xem
   cái gì tới từ đâu, nên tách hẳn ra đáng giá hơn là giống terminal. */
.o-code-console__nhap { color: #1D4ED8; font-weight: 700; }
.o-code-console__lenh { color: #7E22CE; font-weight: 700; }
.o-code-console__he-thong { color: #A8A29E; font-size: 11px; }
.o-code-console__goi-y {
    display: flex;
    gap: .375rem;
    margin-top: .375rem;
    padding: .375rem .5rem;
    border-radius: 3px;
    background: #FFFBEB;
    color: #78350F;
    font-size: 11px;
    line-height: 1.5;
}
.o-code-console__goi-y i { color: #B45309; margin-top: .125rem; }

.o-code-console__go {
    flex: none;
    display: flex;
    align-items: flex-start;
    gap: .5rem;
    padding: .375rem .75rem .5rem;
    border-top: 1px solid var(--ao-vien, #E7E5E4);
    background: #FAFAF9;
}
.o-code-console__go--doi { background: #FFFBEB; border-top-color: #FDE68A; }
.o-code-console__nhac {
    flex: none;
    padding-top: .125rem;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    font-weight: 700;
    color: #A8A29E;
}
.o-code-console__go--doi .o-code-console__nhac { color: #B45309; }
.o-code-console__go textarea {
    flex: 1 1 auto;
    min-width: 0;
    resize: none;
    border: 0;
    outline: none;
    background: transparent;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    line-height: 1.6;
    color: #0D1F33;
}
.o-code-console__go textarea::placeholder { color: #D6D3D1; }

/* ==========================================================================
   TRANG /ide — khung IDE đứng riêng một trang (19/09/2026)
   Cùng bộ lớp .o-code-* với trang Ôn Tập, chỉ khác cái vỏ: ở đây không có câu hỏi nằm cạnh nên
   khung chiếm trọn bề ngang và gần trọn chiều cao màn hình.
   ========================================================================== */
.ide-khung {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: #fff;
    border: 1px solid var(--ao-vien, #E7E5E4);
    border-radius: 4px;
    /* Trừ đi tiêu đề trang, thanh nav, và lề. min-height giữ cho màn hình thấp (hoặc cửa sổ đã bị
       chia đôi) vẫn còn đủ chỗ gõ: dưới ngần này thì ô code chỉ còn vài dòng, không làm bài được.
       Người dùng tự kéo thanh giữa để đổi tỉ lệ ô gõ với console. */
    height: calc(100vh - 230px);
    min-height: 520px;
}
/* Ô gõ phải ăn hết chỗ còn lại sau khi thanh công cụ và console đã lấy phần của chúng. */
.ide-khung .o-code-hang { flex: 1 1 auto; min-height: 0; }
