{"id":2,"date":"2025-03-26T09:53:25","date_gmt":"2025-03-26T09:53:25","guid":{"rendered":"https:\/\/kanetsu.simple.com.vn\/?page_id=2"},"modified":"2025-11-06T05:03:40","modified_gmt":"2025-11-06T05:03:40","slug":"trang-chu","status":"publish","type":"page","link":"https:\/\/namecard.kanetsu.com.vn\/en\/trang-chu\/","title":{"rendered":"Add new"},"content":{"rendered":"\n\t<section class=\"section section-add-new\" id=\"section_428637513\">\n\t\t<div class=\"section-bg fill\" >\n\t\t\t\t\t\t\t\t\t\n\t\t\t\n\n\t\t<\/div>\n\n\t\t\n\n\t\t<div class=\"section-content relative\">\n\t\t\t\n<div class=\"row row-cus row-add\"  id=\"row-151305786\">\n\n\t<div id=\"col-99453305\" class=\"col small-12 large-12\"  >\n\t\t\t\t<div class=\"col-inner\"  >\n\t\t\t\n\t\t\t\n\t<div id=\"text-2125449300\" class=\"text home-page-cus\">\n\t\t\n<!-- ========== A's Base Styles ========== -->\n<style>\n    :root {\n        --cardW: 340px;\n    }\n\n    .bar {\n        display: flex;\n        gap: 10px;\n        align-items: center;\n        width: 100%;\n        margin-left: 0px;\n        justify-content: center;\n        height: 100%;\n    }\n\n    \/* Center the \"Chon Danh Thiep\" control in step A frame *\/\n    #stage-a .wrap {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n    }\n\n    #stage-a .bar {\n        justify-content: center;\n        align-items: center;\n        width: 100%;\n    }\n\n    #stage-a .bar .custom-file-upload {\n        margin: 0 auto;\n    }\n\n\n\n    .item {\n        background: #0e1119;\n        border: 1px solid #232732;\n        border-radius: 16px;\n        overflow: hidden;\n        box-shadow: 0 1px 0 #000 inset\n    }\n\n    .item header {\n        position: static;\n        background: #0e1119;\n        border-bottom: 1px solid #232732\n    }\n\n    .item .meta {\n        display: flex;\n        justify-content: space-between;\n        gap: 8px;\n        align-items: center\n    }\n\n    .item .meta .name {\n        font-size: 13px;\n        opacity: .8;\n        overflow: hidden;\n        text-overflow: ellipsis;\n        white-space: nowrap\n    }\n\n    .panes {\n        display: grid;\n        grid-template-columns: 1fr 1fr\n    }\n\n    .pane {\n        padding: 12px;\n        border-right: 1px solid #232732\n    }\n\n    .pane:last-child {\n        border-right: 0\n    }\n\n    .pane h3 {\n        font-size: 13px;\n        margin: 0 0 8px;\n        opacity: .9;\n        color: #fff;\n    }\n\n    canvas {\n        width: 100%;\n        height: auto;\n        background: #0b0c0f;\n        border: 1px solid #232732;\n        border-radius: 10px\n    }\n\n    \/* .actions {\n        display: flex;\n        flex-wrap: wrap;\n        gap: 8px;\n        padding: 12px;\n        border-top: 1px dashed #232732\n    } *\/\n\n    .hint {\n        font-size: 12px;\n        opacity: .7;\n        color: #fff;\n    }\n\n    \/* Compact flip preview for selected front\/back images *\/\n    .preview-all .preview-side-block {\n        display: none !important;\n        visibility: hidden;\n        height: 0;\n        overflow: hidden;\n        pointer-events: none;\n    }\n\n    .preview-flip-wrap {\n        width: min(460px, 100%);\n        margin: 0 auto;\n    }\n\n    .preview-flip-header {\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n        gap: 10px;\n        margin-bottom: 0px;\n    }\n\n    @media (max-width: 768px) {\n        .form-group-wrapper {\n            padding: 0px 0px;\n        }\n    }\n\n    .preview-flip-face {\n        font-size: 28px;\n        font-weight: 700;\n        color: #132441;\n        line-height: 1.2;\n    }\n\n    .preview-flip-toggle {\n        border: none;\n        border-radius: 10px;\n        padding: 8px 10px;\n        min-width: 42px;\n        width: auto !important;\n        flex: 0 0 auto;\n        display: inline-flex;\n        align-items: center;\n        justify-content: center;\n        background: #132441;\n        color: #fff;\n        font-size: 14px;\n        line-height: 1;\n        cursor: pointer;\n    }\n    .preview-flip-toggle[disabled] {\n        opacity: .5;\n        cursor: not-allowed;\n    }\n\n    .preview-flip-stage {\n        width: 100%;\n        min-height: 260px;\n        border-radius: 14px;\n        overflow: hidden;\n        background: rgba(255, 255, 255, 0.55);\n        border: 1px solid rgba(19, 36, 65, 0.15);\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        position: relative;\n        cursor: pointer;\n        transition: transform .25s ease, box-shadow .25s ease;\n    }\n    .preview-flip-stage.has-image {\n        min-height: 0;\n        background: transparent;\n        border: 0;\n    }\n\n    .preview-flip-stage-inner {\n        width: 100%;\n        height: 100%;\n        min-height: 260px;\n        perspective: 1200px;\n    }\n    .preview-flip-stage.has-image .preview-flip-stage-inner {\n        min-height: 0;\n        aspect-ratio: var(--preview-flip-ratio, 1.75 \/ 1);\n    }\n\n    .preview-flip-card {\n        position: relative;\n        width: 100%;\n        height: 100%;\n        min-height: 260px;\n        transform-style: preserve-3d;\n        transition: transform .45s ease;\n    }\n    .preview-flip-stage.has-image .preview-flip-card {\n        min-height: 0;\n    }\n\n    .preview-flip-stage.is-back .preview-flip-card {\n        transform: rotateY(180deg);\n    }\n\n    .preview-flip-face-panel {\n        position: absolute;\n        inset: 0;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        backface-visibility: hidden;\n        -webkit-backface-visibility: hidden;\n    }\n\n    .preview-flip-face-panel.is-back {\n        transform: rotateY(180deg);\n    }\n\n    .preview-flip-wrap.can-flip .preview-flip-stage:hover {\n        transform: translateY(-2px);\n        box-shadow: 0 8px 24px rgba(19, 36, 65, 0.16);\n    }\n\n    .preview-flip-stage img {\n        width: 100%;\n        height: auto;\n        display: block;\n        border-radius: 10px;\n        object-fit: contain;\n    }\n\n    .preview-flip-hint {\n        margin-top: 8px;\n        font-size: 13px;\n        color: #4a5a74;\n        text-align: center;\n    }\n\n\n\n    .kbd {\n        font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;\n        background: #11131a;\n        border: 1px solid #2a2f3c;\n        padding: 0 6px;\n        border-radius: 6px\n    }\n\n\n\n    #stage-b .custom-file-upload,\n    #stage-b .bar {\n        width: fit-content;\n    }\n\n    #stage-b .custom-file-upload,\n    .home-page-cus #stage-b .btn-delete-all {\n        padding: 12px 30px;\n        color: #fff;\n        background-color: var(--color-main);\n        border: unset;\n        font-weight: 400;\n        border-radius: 8px;\n        text-transform: none;\n        font-size: 20px;\n    }\n\n    .home-page-cus #stage-b .btn-delete-all {\n        margin-bottom: 0;\n        padding: 4px 30px;\n        display: flex;\n        align-items: center;\n        gap: 16px;\n        margin-right: 0;\n    }\n\n    #stage-b img {\n        width: 20px;\n        height: 20px;\n    }\n\n    #stage-b .btn-delete-all img {\n        width: 16px;\n        height: 16px;\n    }\n\n    #stage-b .bar-wrap {\n        display: flex;\n        gap: 20px;\n    }\n\n    .overlay {\n        position: relative\n    }\n\n    .marker {\n        \/* position: absolute; *\/\n        width: 16px;\n        height: 16px;\n        border: 2px solid #5cc8ff;\n        background: #122033;\n        border-radius: 50%;\n        transform: translate(-50%, -50%);\n        cursor: grab\n    }\n\n    .marker:active {\n        cursor: grabbing\n    }\n\n    .overlay .help {\n        position: absolute;\n        left: 8px;\n        bottom: 8px;\n        background: #0b0c0fcc;\n        border: 1px solid #232732;\n        padding: 6px 8px;\n        border-radius: 8px;\n        font-size: 12px\n    }\n\n    \/* Stepper *\/\n    .stepper {\n        bottom: 0;\n        \/* z-index: 20; *\/\n        background: #0b0c0fe6;\n        border-top: 1px solid #232732;\n        display: flex;\n        align-items: center;\n        border-radius: 20px;\n    }\n\n    .stepper {\n        display: none;\n    }\n\n    .stepper:not([hidden]) {\n        display: flex;\n    }\n\n    .stepper .wrap-stage {\n        display: flex;\n        gap: 10px;\n        align-items: center;\n        flex-direction: column;\n        padding: 15px;\n        width: 100%;\n    }\n\n    .steps {\n        display: flex;\n        gap: 6px;\n        align-items: center;\n        font-size: 13px;\n        width: 100%;\n        color: #fff;\n    }\n\n    .steps .dot {\n        width: 10px;\n        height: 8px;\n        border-radius: 50%;\n        background: #2a2f3c\n    }\n\n    .steps .dot.active {\n        background: #5cc8ff\n    }\n\n    \/* ========== B's Form (kept, crop\/camera removed) ========== *\/\n    .stage {\n        display: none\n    }\n\n    #stage-b {\n        position: relative;\n        width: 100%;\n    }\n\n    .bg-blur,\n    #citt-result {\n        width: 100%;\n        max-width: 100%;\n        box-sizing: border-box;\n        border-radius: 20px;\n        border: 0 solid #B5B5B5;\n        background: rgba(0, 0, 0, 0.10);\n        backdrop-filter: blur(30px);\n        padding: 25px 20px;\n        margin-bottom: 32px;\n        overflow: hidden;\n    }\n\n    #citt-result *,\n    #citt-result *::before,\n    #citt-result *::after {\n        box-sizing: border-box;\n    }\n\n\n    #stage-b #citt-result {\n        padding-bottom: 20px;\n        display: none;\n    }\n\n    .stage.active {\n        display: block\n    }\n\n\n\n\n    \/* .preview-all img {\n            width: 100%;\n            height: auto;\n            border: 1px solid #2a2f3c;\n            border-radius: 10px\n        } *\/\n\n\n    \/* .form-layout {\n        display: grid;\n        gap: 6px\n    } *\/\n\n    \/* Gallery for selecting processed images into B *\/\n    .gallery-item {\n        display: grid;\n        grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n        gap: 20px;\n        padding: 12px 0px;\n        margin-top: 30px;\n        padding-bottom: 20px;\n    }\n\n    .card {\n        background: #0e1119;\n        border: 1px solid #232732;\n        border-radius: 14px;\n\n        background: #fff;\n        padding: 20px 10px;\n        padding-bottom: 0;\n    }\n\n    .card .ph {\n\n        font-size: 16px;\n        padding: 10px 0px;\n        border-top: 1px dashed #232732;\n        display: flex;\n        gap: 8px;\n\n    }\n\n    .card .ph button {\n        padding: 0px 8px;\n        border-radius: 8px;\n        padding-bottom: 0;\n        background: #B8B8B8;\n        color: #000;\n        margin-right: 0;\n        text-transform: none;\n        display: flex;\n        gap: 8px;\n        align-items: center;\n        font-weight: 400;\n    }\n\n    .card .ph button.btn-edit-image {\n        position: absolute;\n        top: 10px;\n        right: 10px;\n        z-index: 4;\n        padding: 5px 8px;\n        margin-right: 0;\n        background: #132441;\n        color: #fff;\n        border-radius: 8px;\n        box-shadow: 0 3px 10px rgba(0,0,0,.22);\n        font-weight: 700;\n        font-size: 13px;\n        gap: 4px;\n    }\n\n    .card .ph button.btn-edit-image img {\n        width: 18px !important;\n        height: 18px !important;\n        filter: brightness(0) invert(1);\n    }\n\n\n    .citt-crop-modal {\n        position: fixed;\n        inset: 0;\n        z-index: 999999;\n        background: rgba(7, 18, 34, .72);\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        padding: 18px;\n    }\n\n    .citt-crop-panel {\n        background: #fff;\n        border-radius: 10px;\n        padding: 14px;\n        max-width: calc(100vw - 32px);\n        box-shadow: 0 18px 60px rgba(0, 0, 0, .32);\n    }\n\n    .citt-crop-stage canvas {\n        display: block;\n        max-width: 100%;\n        touch-action: none;\n        cursor: crosshair;\n        background: #111;\n    }\n\n    .citt-crop-actions {\n        display: flex;\n        gap: 8px;\n        justify-content: flex-end;\n        align-items: center;\n        margin-top: 12px;\n    }\n\n    .citt-crop-actions button {\n        border: 0;\n        border-radius: 8px;\n        min-height: 40px;\n        padding: 9px 13px;\n        background: #132441;\n        color: #fff;\n        cursor: pointer;\n        font-size: 13px;\n        font-weight: 700;\n        line-height: 1.15;\n        white-space: nowrap;\n        letter-spacing: 0;\n    }\n\n    .citt-crop-actions button[data-act=\"reset\"],\n    .citt-crop-actions button[data-act=\"cancel\"] {\n        min-width: 64px;\n    }\n\n    .citt-crop-actions button[data-act=\"apply\"] {\n        min-width: 118px;\n    }\n\n    @media (max-width: 480px) {\n        .citt-crop-panel { padding: 10px; }\n        .citt-crop-actions { gap: 6px; }\n        .citt-crop-actions button {\n            min-height: 38px;\n            padding: 8px 10px;\n            font-size: 12px;\n        }\n        .citt-crop-actions button[data-act=\"reset\"],\n        .citt-crop-actions button[data-act=\"cancel\"] {\n            min-width: 58px;\n        }\n        .citt-crop-actions button[data-act=\"apply\"] {\n            min-width: 104px;\n        }\n    }\n\n\n    .citt-edit-image-panel {\n        background: #fff;\n        border-radius: 10px;\n        padding: 18px;\n        width: min(360px, calc(100vw - 36px));\n        box-shadow: 0 18px 60px rgba(0, 0, 0, .32);\n    }\n\n    .citt-edit-image-title {\n        margin: 0 0 14px;\n        color: #132441;\n        font-weight: 700;\n        font-size: 18px;\n    }\n\n    .citt-edit-image-actions {\n        display: grid;\n        gap: 10px;\n    }\n\n    .citt-edit-image-actions button {\n        border: 0;\n        border-radius: 8px;\n        padding: 12px 14px;\n        background: #132441;\n        color: #fff;\n        cursor: pointer;\n        font-weight: 700;\n        text-align: center;\n    }\n\n    .citt-edit-image-actions button[data-act=\"cancel\"] {\n        background: #6b7280;\n    }\n\n    .badge {\n        display: flex;\n        font-size: 11px;\n        padding: 2px 8px;\n        border-radius: 999px;\n        border: 1px solid #2a2f3c;\n        background: #141826;\n        margin-left: 6px;\n        width: 100%;\n        align-items: center;\n    }\n\n    \/* Gallery card active states *\/\n    .gallery-item .card {\n        border: 1px solid #e5e7eb;\n        border-radius: 10px;\n\n        box-shadow: 0 2px 8px rgba(0, 0, 0, .06);\n        transition: box-shadow .2s ease, border-color .2s ease;\n        position: relative;\n    }\n\n    .gallery-item .card .ph {\n        display: flex;\n        align-items: center;\n        display: flex;\n        justify-content: space-between;\n    }\n\n\n    .gallery-item .card .thumb {\n        width: 100%;\n        max-width: 100%;\n        position: relative;\n        height: 170px;\n        overflow: hidden;\n        background: #f3f4f6;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        border-radius: 8px 8px 0 0;\n    }\n\n    \/* Namecard ngang v\u00e0 d\u1ecdc \u0111\u1ec1u v\u1eeba khung \u2014 kh\u00f4ng crop, kh\u00f4ng stretch *\/\n    .gallery-item .card .thumb > img {\n        max-width: 100%;\n        max-height: 100%;\n        width: auto;\n        height: auto;\n        object-fit: contain;\n        display: block;\n    }\n\n    #pick-gallery-item .card .btnDelete {\n        position: absolute;\n        top: -30px;\n        right: -30px;\n        background: #fff;\n        border: none;\n        border-radius: 50%;\n        width: 30px;\n        height: 30px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        z-index: 10;\n        transition: background 0.2s ease;\n        padding: 0;\n        font-size: 10px;\n        box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);\n    }\n\n    .gallery-item .card .btnDelete:hover {\n        background: rgba(220, 38, 38, 0.8);\n    }\n\n    #pick-gallery-item .card .btnDelete img {\n        width: 12px;\n        height: 12px;\n    }\n\n    .gallery-item .card.is-front {\n        border-color: #3b82f6;\n        box-shadow: 0 4px 16px rgba(59, 130, 246, .2);\n    }\n\n    .gallery-item .card.is-back {\n        border-color: #22c55e;\n        box-shadow: 0 4px 16px rgba(34, 197, 94, .2);\n    }\n\n    .gallery-item .card .tag {\n        position: absolute;\n        top: 10px;\n        left: 10px;\n        padding: 4px 8px;\n        border-radius: 999px;\n        font: 600 12px\/1 \"Inter\", system-ui;\n        color: #fff;\n    }\n\n    \/* Highlight target card while main camera will replace it *\/\n    .gallery-item .card.camera-target {\n        box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.12), 0 6px 18px rgba(59, 130, 246, 0.08);\n        border-color: #3b82f6;\n    }\n\n\n\n    .gallery-item .card .ph button[data-act=\"front\"].is-active {\n        background-color: var(--color-main);\n        color: #fff;\n    }\n\n    .gallery-item .card .ph button[data-act=\"back\"].is-active {\n        background-color: var(--color-main);\n        color: #fff;\n    }\n\n    #camera-container {\n        border-radius: 12px;\n        margin: 16px 0;\n        min-width: 100%;\n    }\n\n    #cam-video {\n        width: 100%;\n        border-radius: 10px;\n    }\n\n    .cam-wrap {\n        width: 100%;\n    }\n\n    .cam-controls {\n        display: grid;\n        grid-template-columns: 1fr 1fr;\n        gap: 8px;\n        align-items: stretch;\n        position: relative;\n    }\n\n    .cam-capture-left {\n        display: contents;\n    }\n\n    .cam-controls .cam-done {\n        \/* Row 2, Col 1 - with display:contents on parent, this becomes grid item *\/\n    }\n\n    .cam-controls .cam-cancel {\n        \/* Row 2, Col 2 *\/\n    }\n\n    .cam-controls button {\n        padding: 8px 16px;\n        border-radius: 8px;\n        border: 1px solid #B5B5B5;\n        background: #fff;\n        cursor: pointer;\n        font-size: 12px;\n        display: flex;\n        height: fit-content;\n        align-items: center;\n        gap: 8px;\n        margin-right: 0;\n        text-transform: none;\n        color: var(--color-main);\n        box-shadow: 2px 4px 4px 0 rgba(0, 0, 0, 0.20);\n\n    }\n\n\n\n    .cam-controls #cam-cancel img {\n        width: 18px;\n        height: 18px;\n\n    }\n\n    .cam-controls #cam-capture {\n        font-weight: 600;\n    }\n\n    .cam-controls #cam-done[disabled] {\n        opacity: .5;\n        cursor: not-allowed;\n    }\n\n    @media (max-width: 768px) {\n        \/* Layout: 2 rows \u00d7 2 columns grid *\/\n        .cam-controls {\n            display: grid;\n            grid-template-columns: 1fr 1fr;\n            gap: 8px;\n            align-items: stretch;\n        }\n\n        \/* Row 1, Col 1-2: Capture & Horizontal buttons *\/\n        .cam-capture-left {\n            display: contents;\n        }\n\n        \/* Row 1, Col 1: Capture button *\/\n        .cam-controls #cam-capture,\n        .cam-controls .cam-capture {\n            justify-content: center;\n        }\n\n        \/* Row 1, Col 2: Horizontal button *\/\n        .cam-controls #cam-toggle-orient,\n        .cam-controls .cam-toggle-orient {\n            justify-content: center;\n            background: #fff;\n            color: var(--color-main);\n            border: 1px solid #B5B5B5;\n            font-weight: 600;\n        }\n\n        \/* Row 2, Col 1: Use-photos button *\/\n        .cam-controls #cam-done,\n        .cam-controls .cam-done {\n            justify-content: center;\n            margin-top: 0;\n        }\n\n        \/* Row 2, Col 2: Close button *\/\n        .cam-controls #cam-cancel,\n        .cam-controls .cam-cancel {\n            padding: 10px 4px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            margin-left: 10px;\n        }\n        \/* Uniform color: override dark toggle-orient to white *\/\n        .cam-controls #cam-toggle-orient,\n        .cam-controls .cam-toggle-orient {\n            background: #fff;\n            color: var(--color-main);\n            border: 1px solid #B5B5B5;\n            font-weight: 600;\n        }\n    }\n\n    .cam-thumbs {\n        display: flex;\n        gap: 10px;\n        overflow-x: auto;\n        padding: 8px 0;\n    }\n\n    .cam-thumbs .t {\n        position: relative;\n    }\n\n    .cam-thumbs img {\n        width: auto;\n        display: block;\n        border-radius: 6px;\n        max-width: 200px;\n    }\n\n    .cam-thumbs .rm {\n        position: absolute;\n        top: -8px;\n        right: -14px;\n        background: #fff;\n        border: none;\n        border-radius: 50%;\n        width: 24px;\n        height: 24px;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n        z-index: 10;\n        transition: background 0.2s ease;\n        padding: 0;\n        font-size: 10px;\n        box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.25);\n    }\n\n    input[type=\"file\"] {\n        display: none;\n    }\n\n    @media (max-width:768px) {\n        .panes {\n            grid-template-columns: 1fr\n        }\n\n        .gallery-item {\n            display: flex;\n            overflow-x: auto;\n            margin-top: 20px;\n        }\n\n        .gallery-item .card {\n            min-width: 288px;\n            width: 100%;\n        }\n\n        .sticky .bar {\n            flex-direction: column;\n        }\n\n        .update-camera-container .cam-controls button {\n            padding: 8px 16px;\n            border-radius: 8px;\n            border: 1px solid #B5B5B5;\n            background: #fff;\n            cursor: pointer;\n            font-size: 12px;\n            display: flex;\n            height: fit-content;\n            align-items: center;\n            gap: 8px;\n            margin-right: 0;\n            text-transform: none;\n            color: var(--color-main);\n            box-shadow: 2px 4px 4px 0 rgba(0, 0, 0, 0.20);\n        }\n\n        .update-camera-container .cam-controls {\n            display: grid;\n            grid-template-columns: 1fr 1fr;\n            gap: 8px;\n        }\n\n        .update-camera-container .cam-capture-left {\n            display: contents;\n        }\n\n        .update-camera-container .cam-controls .cam-cancel {\n            margin-left: 10px;\n        }\n\n\n        #stage-b .cam-cancel img {\n            width: 12px;\n            height: 12px;\n        }\n\n        .cam-controls #cam-cancel img {\n            width: 12px;\n            height: 12px;\n        }\n\n        .cam-controls #cam-capture,\n        .cam-controls #cam-done[disabled],\n        .cam-controls #cam-done {\n            margin-bottom: 0px;\n        }\n    }\n\n    @media (max-width: 768px) {\n        #pick-gallery-item .card .ph .update-take-photo {\n            display: block;\n            font-size: 12px;\n            border: 0.75px solid var(--Color, #b5b5b5);\n            box-shadow: none;\n            background: #b8b8b8;\n            padding: 4px 12px;\n        }\n    }\n\n    @media (max-width: 768px) {\n        .card .ph button.btn-replace-cropped, .gallery-item .card .ph #start-camera {\n            position: unset !important;\n            padding: 4px 12px !important;\n            font-size: 12px;\n            border: 0.75px solid var(--Color, #b5b5b5);\n            box-shadow: none;\n            border-radius: 8px;\n            line-height: 14px;\n        }\n    }\n\n\n    \/* === FIX CAMERA: guide overlay + auto orientation + mirror preview === *\/\n    .height-cam {\n        position: relative;\n        height: 50vh;\n        max-height: 350px;\n        background: #000;\n        border-radius: 12px;\n        overflow: hidden;\n    }\n    .height-cam.is-portrait {\n        height: 50vh;\n        max-height: 350px;\n    }\n\n    #cam-video {\n        width: 100%;\n        height: 100%;\n        object-fit: cover;\n    }\n\n    .cam-guide,\n    #cam-guide {\n        position: absolute;\n        inset: 0;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        pointer-events: none;\n    }\n\n    .cam-guide-box,\n    #cam-guide-box {\n        width: 86%;\n        max-width: 760px;\n        aspect-ratio: auto;\n        \/* m\u00e1\u00ba\u00b7c \u00c4\u2018\u00e1\u00bb\u2039nh NGANG *\/\n        border: 2px solid rgba(255, 255, 255, .95);\n        border-radius: 12px;\n        box-shadow: 0 0 0 9999px rgba(0, 0, 0, .35);\n    }\n\n    #cam-toggle-orient {\n        margin: 0px 8px;\n        padding: 8px 12px;\n        border-radius: 10px;\n        border: 1px solid rgba(255, 255, 255, .35);\n        background: rgba(0, 0, 0, .35);\n        color: #fff;\n        font-weight: 700;\n        letter-spacing: .3px;\n        cursor: pointer;\n        height: fit-content;\n    }\n\n    #cam-toggle-orient:disabled {\n        opacity: .55;\n        cursor: not-allowed;\n    }\n\n    #camera-container.is-mirror #cam-video {\n        transform: scaleX(-1);\n    }\n\n    \/* === Per-card camera container: match main camera height === *\/\n    .update-camera-container .height-cam {\n        position: relative;\n        height: 50vh;\n        max-height: 350px;\n        background: #000;\n        border-radius: 12px;\n        overflow: hidden;\n    }\n    .update-camera-container .height-cam.is-portrait {\n        height: 50vh;\n        max-height: 350px;\n    }\n\n    .update-camera-container .cam-video {\n        width: 100%;\n        height: 100%;\n        object-fit: cover;\n    }\n\n    .update-camera-container .cam-guide {\n        position: absolute;\n        inset: 0;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        pointer-events: none;\n    }\n\n    .update-camera-container .cam-guide-box {\n        width: 86%;\n        max-width: 760px;\n        aspect-ratio: auto;\n        border: 2px solid rgba(255, 255, 255, .95);\n        border-radius: 12px;\n        box-shadow: 0 0 0 9999px rgba(0, 0, 0, .35);\n    }\n<\/style>\n\n\n<!-- Font Awesome (for icons that B used) -->\n<link rel=\"stylesheet\" href=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/6.4.0\/css\/all.min.css\">\n\n<!-- OpenCV.js v\u00c3\u00a0 EXIF.js \u00c4\u2018\u00c3\u00a3 \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c load b\u00e1\u00bb\u0178i plugin name-card-user -->\n<!-- Kh\u00c3\u00b4ng c\u00e1\u00ba\u00a7n load l\u00e1\u00ba\u00a1i \u00c4\u2018\u00e1\u00bb\u0192 tr\u00c3\u00a1nh xung \u00c4\u2018\u00e1\u00bb\u2122t -->\n<!-- JSZip (A) -->\n<script async src=\"https:\/\/cdn.jsdelivr.net\/npm\/jszip@3.10.1\/dist\/jszip.min.js\"><\/script>\n\n\n<body>\n\n    <!-- ===== STEP 1: A \u00e2\u20ac\u201d Bulk upload & auto-fix ===== -->\n    <section id=\"stage-a\" class=\"stage active\" aria-labelledby=\"title-fix-ai\">\n        <div class=\"sticky\">\n            <div class=\"wrap\">\n                <h1 id=\"title-fix-ai\">Add New<\/h1>\n                <div class=\"bar\">\n                    <button id=\"start-camera\" type=\"button\"><img decoding=\"async\"\n                            src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/camera.svg\" alt=\"\"> Take Photos<\/button>\n                    <label for=\"fileInput\" class=\"custom-file-upload\">\n                        <img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/clarity_list-solid.svg\"> Select Name Card                    <\/label>\n                    <input id=\"fileInput\" class=\"a-input\" type=\"file\" accept=\"image\/*\" multiple \/>\n\n\n                <\/div>\n                <!-- Loading indicator khi t\u00e1\u00ba\u00a3i \u00e1\u00ba\u00a3nh -->\n                <div id=\"image-upload-indicator\" class=\"image-upload-indicator\" style=\"display: none;\">\n                    <div class=\"upload-progress\">\n                        <div class=\"upload-spinner\"><\/div>\n                        <span class=\"upload-text\">Uploading images...<\/span>\n                    <\/div>\n                <\/div>\n            <\/div>\n        <\/div>\n\n        <div id=\"grid\" class=\"grid grid-camera\" aria-live=\"polite\"><\/div>\n        <div id=\"camera-container\" style=\"display:none\">\n            <div class=\"cam-wrap\">\n                <div class=\"height-cam\">\n                    <video id=\"cam-video\" playsinline autoplay muted><\/video>\n                    <!-- Guide overlay: v\u00c3\u00b9ng ch\u00e1\u00bb\u00a5p namecard -->\n                    <div id=\"cam-guide\" aria-hidden=\"true\">\n                        <div id=\"cam-guide-box\"><\/div>\n                    <\/div>\n                <\/div>\n\n                <div class=\"cam-controls\">\n                    <div class=\"cam-capture-left\">\n                        <button id=\"cam-capture\" type=\"button\"><img decoding=\"async\"\n                                src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/camera.svg\"\n                                alt=\"\">Capture<\/button>\n                        <button id=\"cam-toggle-orient\" type=\"button\"\n                            title=\"Toggle frame: AUTO \/ Horizontal \/ Vertical\">AUTO<\/button>\n                        <button id=\"cam-done\" type=\"button\" disabled>&#10004; Use <span id=\"cam-count\">0<\/span> photos<\/button>\n                    <\/div>\n                    <button id=\"cam-cancel\" type=\"button\"><img decoding=\"async\"\n                            src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/cancel2.png\" alt=\"\"><\/button>\n                <\/div>\n\n                <div id=\"cam-thumbs\" class=\"cam-thumbs\" aria-live=\"polite\"><\/div>\n            <\/div>\n        <\/div>\n    <\/section>\n\n    <!-- ===== STEP 2: B \u00e2\u20ac\u201d OCR form (crop\/camera removed) ===== -->\n    <section id=\"stage-b\" class=\"stage\" aria-labelledby=\"h-b\">\n        <div class=\"bg-blur\">\n            <header class=\"sticky\">\n                <div class=\"wrap\">\n                    <div>\n                        <h1 id=\"h-b\" class=\"title-step\">Select front\/back &amp; Convert Text<\/h1>\n                    <\/div>\n\n                    <div class=\"bar-wrap\">\n                        <div class=\"bar\">\n                            <label class=\"custom-file-upload add-more-photos-label\" role=\"button\" tabindex=\"0\">\n                                <img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/ic_round-plus.png\">\n                                Add more images                            <\/label>\n                            <input id=\"fileInput\" class=\"a-input\" type=\"file\" accept=\"image\/*\" multiple \/>\n                            <button id=\"start-camera-add-more\" type=\"button\">\ud83d\udcf7 Open camera<\/button>\n\n                        <\/div>\n                        <div>\n                            <button class=\"btn-delete-all\" type=\"button\"> <img decoding=\"async\"\n                                    src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/cancel.png\">\n                                Delete all<\/button>\n                        <\/div>\n\n                    <\/div>\n\n                <\/div>\n            <\/header>\n\n\n            <!-- Loading indicator khi t\u00e1\u00ba\u00a3i \u00e1\u00ba\u00a3nh -->\n            <div id=\"image-upload-indicator-b\" class=\"image-upload-indicator\" style=\"display: none;\">\n                <div class=\"upload-progress\">\n                    <div class=\"upload-spinner\"><\/div>\n                    <span class=\"upload-text\">Uploading images...<\/span>\n                <\/div>\n            <\/div>\n            <!-- Gallery of processed results to pick for front\/back -->\n            <div id=\"pick-gallery-item\" class=\"gallery-item\" aria-live=\"polite\"><\/div>\n            <div class=\"addnew-deleteAll\">\n                <div class=\"bar-wrap\">\n                    <div class=\"bar\">\n                        <label for=\"fileInput\" class=\"custom-file-upload\" id=\"upload-list-img\">\n                            <img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/ic_round-plus.png\">\n                            Add more images                        <\/label>\n                        <input id=\"fileInput\" class=\"a-input\" type=\"file\" accept=\"image\/*\" multiple \/>\n                        <button id=\"start-camera\" type=\"button\">\ud83d\udcf7 Open camera<\/button>\n\n                    <\/div>\n                    <div>\n                        <button class=\"btn-delete-all\" type=\"button\"> <img decoding=\"async\"\n                                src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/cancel.png\">\n                            Delete all<\/button>\n                    <\/div>\n\n                <\/div>\n            <\/div>\n        <\/div>\n\n        <!-- B's form (kept, without cropper\/camera) -->\n        <form id=\"citt-upload-form\" class=\"citt-upload-form-cus\" method=\"post\" enctype=\"multipart\/form-data\"\n            class=\"wrap\" style=\"padding-top:0; display:none;\">\n\n            <div class=\"form-all-wapper\">\n\n                <div class=\"form-group-wrapper\">\n                    <div class=\"image-input\">\n                        <!-- Hidden real inputs (we will populate programmatically) -->\n                        <input type=\"file\" id=\"image-before\" name=\"citt_image_before\" accept=\"image\/*\"\n                            style=\"display:none;\">\n                        <input type=\"file\" id=\"image-after\" name=\"citt_image\" accept=\"image\/*\"\n                            style=\"display:none;\">\n\n                        <div class=\"preview-all\">\n    <div id=\"preview-flip-wrap\" class=\"preview-flip-wrap\" data-face=\"front\">\n        <div class=\"preview-flip-header\">\n            <span id=\"preview-flip-face\" class=\"preview-flip-face\">Front side<\/span>\n            <button id=\"preview-flip-btn\" type=\"button\" class=\"preview-flip-toggle\" style=\"display:none;\" aria-label=\"Lat mat\" title=\"Lat mat\"><i class=\"fa fa-random\" aria-hidden=\"true\"><\/i><\/button>\n        <\/div>\n        <div id=\"preview-flip-stage\" class=\"preview-flip-stage\">\n            <div class=\"hint\">No front side selected<\/div>\n        <\/div>\n        <div class=\"preview-flip-hint\">Touch the image to flip between front and back<\/div>\n    <\/div>\n    <div class=\"preview-side-block\">\n        <span>Back side<\/span>\n        <div id=\"preview-after\">\n            <div class=\"hint\">No back side selected<\/div>\n        <\/div>\n    <\/div>\n    <div class=\"preview-side-block\">\n        <span>Front side<\/span>\n        <div id=\"preview-before\">\n            <div class=\"hint\">No front side selected<\/div>\n        <\/div>\n    <\/div>\n<\/div>\n\n\n                        <\/div>\n                    <\/div>\n                <\/div>\n\n                <!-- The rest of B's dependent selects\/checkboxes kept as-is -->\n                <div class=\"form-all-choose-job\" style=\"display:none;\">\n                    <div class=\"form-layout-wrapper\" style=\"display:none;\">\n                        <div class=\"form-layout\">\n\n                            <select name=\"country\" id=\"country\" >\n                                <option value=\"\"> Ch\u00e1\u00bb\u008dn qu\u00e1\u00bb\u2018c gia * <\/option>\n                                <option value=\"Hongkong\">H\u00e1\u00bb\u201cng K\u00c3\u00b4ng<\/option>\n                                <option value=\"Holland\">H\u00c3\u00a0 Lan<\/option>\n                                <option value=\"India\">\u00e1\u00ba\u00a4n \u00c4\u0090\u00e1\u00bb\u2122<\/option>\n                                <option value=\"Japan\">Nh\u00e1\u00ba\u00adt B\u00e1\u00ba\u00a3n<\/option>\n                                <option value=\"Korea\">H\u00c3\u00a0n Qu\u00e1\u00bb\u2018c<\/option>\n                                <option value=\"Philippines\">Philippines<\/option>\n                                <option value=\"Thailand\">Th\u00c3\u00a1i Lan<\/option>\n                                <option value=\"UK\">Anh<\/option>\n                                <option value=\"USA\">Hoa K\u00e1\u00bb\u00b3<\/option>\n                                <option value=\"Vietnam Bac\">Vi\u00e1\u00bb\u2021t Nam (Mi\u00e1\u00bb\u0081n B\u00e1\u00ba\u00afc)<\/option>\n                                <option value=\"Vietnam Trung\">Vi\u00e1\u00bb\u2021t Nam (Mi\u00e1\u00bb\u0081n Trung)<\/option>\n                                <option value=\"Vietnam Nam\">Vi\u00e1\u00bb\u2021t Nam (Mi\u00e1\u00bb\u0081n Nam)<\/option>\n                                <option value=\"China\">Trung Qu\u00e1\u00bb\u2018c<\/option>\n                                <option value=\"Campuchia\">Campuchia<\/option>\n                                <option value=\"Taiwan\">\u00c4\u0090\u00c3\u00a0i Loan<\/option>\n                                <option value=\"L\u00c3\u00a0o\">L\u00c3\u00a0o<\/option>\n                                <option value=\"Singapore\">Singapore<\/option>\n                            <\/select>\n                        <\/div>\n\n                        <div id=\"region-select\" style=\"display:none;\">\n                            <label>Ch\u00e1\u00bb\u008dn Mi\u00e1\u00bb\u0081n (ch\u00e1\u00bb\u2030 \u00c3\u00a1p d\u00e1\u00bb\u00a5ng cho Vi\u00e1\u00bb\u2021t Nam):<\/label>\n                            <select name=\"region\">\n                                <option value=\"\">-- Ch\u00e1\u00bb\u008dn Mi\u00e1\u00bb\u0081n --<\/option>\n                                <option value=\"Mi\u00e1\u00bb\u0081n B\u00e1\u00ba\u00afc\">Mi\u00e1\u00bb\u0081n B\u00e1\u00ba\u00afc<\/option>\n                                <option value=\"Mi\u00e1\u00bb\u0081n Trung\">Mi\u00e1\u00bb\u0081n Trung<\/option>\n                                <option value=\"Mi\u00e1\u00bb\u0081n Nam\">Mi\u00e1\u00bb\u0081n Nam<\/option>\n                            <\/select>\n                        <\/div>\n\n                        <div class=\"form-layout\">\n\n                            <select name=\"supplier_type\" id=\"supplier_type\">\n                                <option value=\"\"> Ch\u00e1\u00bb\u008dn <\/option>\n                                <option value=\"Business\">Kinh doanh<\/option>\n                                <option value=\"Service\">D\u00e1\u00bb\u2039ch v\u00e1\u00bb\u00a5<\/option>\n                            <\/select>\n                        <\/div>\n                    <\/div>\n\n                    <div id=\"supplier-options\" style=\"display:none;margin-top:10px;\">\n                        <div id=\"business-options\" style=\"display:none;\">\n                            <p>Th\u00c3\u00b4ng tin cho Business:<\/p>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"business_option[]\" value=\"Customer\">\n                                Kh\u00c3\u00a1ch h\u00c3\u00a0ng\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"business_option[]\" value=\"Manufacturer\">\n                                Nh\u00c3\u00a0 s\u00e1\u00ba\u00a3n xu\u00e1\u00ba\u00a5t\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"business_option[]\" value=\"Manufacturing Trading\">\n                                Th\u00c6\u00b0\u00c6\u00a1ng m\u00e1\u00ba\u00a1i\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"business_option[]\" value=\"Others\">\n                                Kh\u00c3\u00a1c\n                            <\/label>\n                        <\/div>\n                        <div id=\"service-options\" style=\"display:none;\">\n                            <p>Th\u00c3\u00b4ng tin cho Service:<\/p>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Accounting & Auditing\">\n                                K\u00e1\u00ba\u00bf to\u00c3\u00a1n & Ki\u00e1\u00bb\u0192m to\u00c3\u00a1n\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Assurance\">\n                                B\u00e1\u00ba\u00a3o hi\u00e1\u00bb\u0192m\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Bank\">\n                                Ng\u00c3\u00a2n h\u00c3\u00a0ng\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Consulting: Visa, TRC, License\">\n                                T\u00c6\u00b0 v\u00e1\u00ba\u00a5n: Visa, TRC, Gi\u00e1\u00ba\u00a5y ph\u00c3\u00a9p\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Finance\">\n                                T\u00c3\u00a0i ch\u00c3\u00adnh\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Forwarder\">\n                                Giao nh\u00e1\u00ba\u00adn\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Governmental Administration\">\n                                H\u00c3\u00a0nh ch\u00c3\u00adnh nh\u00c3\u00a0 n\u00c6\u00b0\u00e1\u00bb\u203ac\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Healthcare\">\n                                Y t\u00e1\u00ba\u00bf\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Hotel\">\n                                Kh\u00c3\u00a1ch s\u00e1\u00ba\u00a1n\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Information Technology\">\n                                C\u00c3\u00b4ng ngh\u00e1\u00bb\u2021 th\u00c3\u00b4ng tin\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Infrastructure\">\n                                C\u00c6\u00a1 s\u00e1\u00bb\u0178 h\u00e1\u00ba\u00a1 t\u00e1\u00ba\u00a7ng\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Law Association\">\n                                Lu\u00e1\u00ba\u00adt, Hi\u00e1\u00bb\u2021p h\u00e1\u00bb\u2122i\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Leisure\">\n                                Gi\u00e1\u00ba\u00a3i tr\u00c3\u00ad\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Real Estate\">\n                                B\u00e1\u00ba\u00a5t \u00c4\u2018\u00e1\u00bb\u2122ng s\u00e1\u00ba\u00a3n\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Recruitment\">\n                                Tuy\u00e1\u00bb\u0192n d\u00e1\u00bb\u00a5ng\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Stationery & office supplies\">\n                                V\u00c4\u0192n ph\u00c3\u00b2ng ph\u00e1\u00ba\u00a9m & v\u00e1\u00ba\u00adt t\u00c6\u00b0 v\u00c4\u0192n ph\u00c3\u00b2ng\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Taxi\">\n                                Taxi\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Technology and the like\">\n                                C\u00c3\u00b4ng ngh\u00e1\u00bb\u2021 v\u00c3\u00a0 c\u00c3\u00a1c d\u00e1\u00bb\u2039ch v\u00e1\u00bb\u00a5 t\u00c6\u00b0\u00c6\u00a1ng t\u00e1\u00bb\u00b1\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"service_option[]\" value=\"Website\">\n                                Website\n                            <\/label>\n\n                        <\/div>\n                    <\/div>\n\n                    <div id=\"industry-wrapper\">\n                        <p id=\"industry-label\">Ch\u00e1\u00bb\u008dn l\u00c4\u00a9nh v\u00e1\u00bb\u00b1c:<\/p>\n                        <div id=\"industry-group\">\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Agriculture\">\n                                N\u00c3\u00b4ng nghi\u00e1\u00bb\u2021p\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Automobile\">\n                                \u00c3\u201d t\u00c3\u00b4\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Energy\">\n                                N\u00c4\u0192ng l\u00c6\u00b0\u00e1\u00bb\u00a3ng\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Factory\">\n                                Nh\u00c3\u00a0 m\u00c3\u00a1y\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Home Appliances\">\n                                Thi\u00e1\u00ba\u00bft b\u00e1\u00bb\u2039 gia d\u00e1\u00bb\u00a5ng\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Industrial Equipment\">\n                                Thi\u00e1\u00ba\u00bft b\u00e1\u00bb\u2039 c\u00c3\u00b4ng nghi\u00e1\u00bb\u2021p\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Medical\">\n                                Y t\u00e1\u00ba\u00bf\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Oil & Gas\">\n                                D\u00e1\u00ba\u00a7u kh\u00c3\u00ad\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"industry[]\" value=\"Railway Industry\">\n                                Ng\u00c3\u00a0nh \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u009dng s\u00e1\u00ba\u00aft\n                            <\/label>\n\n                        <\/div>\n                    <\/div>\n\n                    <div id=\"sub-options\" style=\"display:none;margin-top:10px;\">\n                        <label>C\u00c3\u00a1c ng\u00c3\u00a0nh ph\u00e1\u00bb\u00a5:<\/label>\n                        <div id=\"medical-sub\" style=\"display:none;\">\n                            <label>\n                                <input type=\"checkbox\" name=\"sub_industry[]\" value=\"Hospital\">\n                                B\u00e1\u00bb\u2021nh vi\u00e1\u00bb\u2021n\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"sub_industry[]\" value=\"Ticket\">\n                                V\u00c3\u00a9\n                            <\/label>\n\n                        <\/div>\n                        <div id=\"factory-sub\" style=\"display:none;\">\n                            <label>\n                                <input type=\"checkbox\" name=\"sub_industry[]\" value=\"Manufacturer\">\n                                Nh\u00c3\u00a0 s\u00e1\u00ba\u00a3n xu\u00e1\u00ba\u00a5t\n                            <\/label>\n\n                            <label>\n                                <input type=\"checkbox\" name=\"sub_industry[]\" value=\"Manufacturing Trading\">\n                                S\u00e1\u00ba\u00a3n xu\u00e1\u00ba\u00a5t - Th\u00c6\u00b0\u00c6\u00a1ng m\u00e1\u00ba\u00a1i\n                            <\/label>\n\n                        <\/div>\n                    <\/div>\n                <\/div>\n\n                <div class=\"form-submit-cus\" style=\"display:none;\">\n                    <button class=\"submit-form form-submit primary\" type=\"submit\"> <img decoding=\"async\"\n                            src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/upload.png\" alt=\"T&#x1EA3;i l&#xEA;n\"> T&#x1EA3;i\n                        L&#xEA;n &amp; Chuy&#x1EC3;n Text<\/button>\n                <\/div>\n\n        <\/form>\n\n        <div id=\"citt-result\" class=\"wrap\"><\/div>\n    <\/section>\n\n    <!-- ===== Sticky stepper ===== -->\n    <div class=\"stepper\" hidden style=\"display:none;\">\n        <div class=\"wrap\">\n            <div class=\"steps\">\n                <div class=\"dot\" id=\"dot-a\"><\/div>\n\n                <span class=\"badge\" id=\"count-fixed\">0 \u00e1\u00ba\u00a3nh \u00c4\u2018\u00c3\u00a3 x\u00e1\u00bb\u00ad l\u00c3\u00bd<\/span>\n                <span style=\"opacity:.5\">\u00e2\u20ac\u00a2<\/span>\n                <div class=\"dot\" id=\"dot-b\"><\/div>\n\n            <\/div>\n            <div style=\"display:flex;gap:8px\">\n                <button id=\"go-prev\" disabled>Quay l\u00e1\u00ba\u00a1i<\/button>\n                <button id=\"go-next\" class=\"primary\" disabled>Ti\u00e1\u00ba\u00bfp t\u00e1\u00bb\u00a5c \u00e2\u2020\u2019<\/button>\n            <\/div>\n        <\/div>\n    <\/div>\n\n\n\n    <script>\n        \/\/ === Selection state (to ensure each card only 1 role at a time) ===\n        let selectedFrontId = null; \/\/ rec.id cho M\u00e1\u00ba\u00b7t tr\u00c6\u00b0\u00e1\u00bb\u203ac\n        let selectedBackId = null; \/\/ rec.id cho M\u00e1\u00ba\u00b7t sau\n        const ICON_DEFAULT = '\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/namecard.png';\n        const ICON_ACTIVE = '\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/namecard2.png';\n\n        function getUploadSubmitElements() {\n            const form = document.getElementById('citt-upload-form');\n            const stageB = document.getElementById('stage-b');\n            const submitWrap = (form && form.querySelector('.form-submit-cus')) ||\n                (stageB && stageB.querySelector('.form-submit-cus')) ||\n                null;\n            const submitBtn = (submitWrap && submitWrap.querySelector('button[type=\"submit\"]')) ||\n                (form && form.querySelector('button[type=\"submit\"]')) ||\n                null;\n            return { form, submitWrap, submitBtn };\n        }\n\n        function syncSubmitButtonVisibility() {\n            const { form, submitWrap, submitBtn } = getUploadSubmitElements();\n            const hasFrontPreview = !!document.querySelector('#preview-before img');\n            const hasBackPreview = !!document.querySelector('#preview-after img');\n            const hasFlipPreview = !!document.querySelector('#preview-flip-stage img');\n            const frontInput = document.getElementById('image-before');\n            const backInput = document.getElementById('image-after');\n            const hasFrontFile = !!(frontInput && frontInput.files && frontInput.files.length > 0);\n            const hasBackFile = !!(backInput && backInput.files && backInput.files.length > 0);\n            const shouldShow = !!selectedFrontId || !!selectedBackId || hasFrontPreview || hasBackPreview || hasFlipPreview || hasFrontFile || hasBackFile;\n\n            if (submitWrap) submitWrap.style.display = shouldShow ? '' : 'none';\n            if (form) form.style.display = shouldShow ? 'block' : form.style.display;\n            if (submitBtn) {\n                submitBtn.style.display = '';\n                submitBtn.disabled = !shouldShow;\n                if (shouldShow) {\n                    submitBtn.innerHTML = '<img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/upload.png\" alt=\"T&#x1EA3;i l&#xEA;n\"> T&#x1EA3;i L&#xEA;n &amp; Chuy&#x1EC3;n Text';\n                }\n            }\n        }\n\n        function escapeAttr(value) {\n            return String(value || '')\n                .replace(\/&\/g, '&amp;')\n                .replace(\/\"\/g, '&quot;')\n                .replace(\/<\/g, '&lt;')\n                .replace(\/>\/g, '&gt;');\n        }\n\n        function syncFlipPreviewRatio(stage, src) {\n            if (!stage || !src) return;\n            stage.classList.add('has-image');\n            const img = new Image();\n            img.onload = function() {\n                if (!img.naturalWidth || !img.naturalHeight) return;\n                stage.style.setProperty('--preview-flip-ratio', img.naturalWidth + ' \/ ' + img.naturalHeight);\n            };\n            img.src = src;\n        }\n\n        function getPreviewSrc(img) {\n            return img ? (img.dataset.previewSrc || img.currentSrc || img.getAttribute('src') || '') : '';\n        }\n\n        function resetFlipPreviewRatio(stage) {\n            if (!stage) return;\n            stage.classList.remove('has-image');\n            stage.style.removeProperty('--preview-flip-ratio');\n            stage.removeAttribute('data-front-src');\n            stage.removeAttribute('data-back-src');\n        }\n\n        function syncFlipPreview() {\n            const wrap = document.getElementById('preview-flip-wrap');\n            const stage = document.getElementById('preview-flip-stage');\n            const faceLabel = document.getElementById('preview-flip-face');\n            const flipBtn = document.getElementById('preview-flip-btn');\n            const previewBefore = document.getElementById('preview-before');\n            const previewAfter = document.getElementById('preview-after');\n            if (!wrap || !stage || !faceLabel || !flipBtn || !previewBefore || !previewAfter) return;\n\n            const frontImg = previewBefore.querySelector('img');\n            const backImg = previewAfter.querySelector('img');\n            const frontSrc = getPreviewSrc(frontImg);\n            const backSrc = getPreviewSrc(backImg);\n\n            if (!frontSrc && !backSrc) {\n                wrap.classList.remove('can-flip');\n                wrap.dataset.face = 'front';\n                faceLabel.textContent = 'Front side';\n                flipBtn.style.display = 'none';\n                stage.classList.remove('is-back');\n                resetFlipPreviewRatio(stage);\n                stage.innerHTML = '<div class=\"hint\">No front side selected<\/div>';\n                return;\n            }\n\n            if (wrap.dataset.face !== 'front' && wrap.dataset.face !== 'back') {\n                wrap.dataset.face = 'front';\n            }\n            if (wrap.dataset.face === 'back' && !backSrc) {\n                wrap.dataset.face = 'front';\n            }\n\n            const canFlip = !!(frontSrc && backSrc);\n            wrap.classList.toggle('can-flip', canFlip);\n            flipBtn.style.display = canFlip ? 'inline-flex' : 'none';\n            flipBtn.disabled = !canFlip;\n\n            const showBack = (wrap.dataset.face === 'back' && !!backSrc);\n            const activeSrc = showBack ? backSrc : (frontSrc || backSrc);\n            faceLabel.textContent = showBack ? 'Back side' : 'Front side';\n            stage.classList.toggle('is-back', canFlip && showBack);\n\n            if (!activeSrc) {\n                resetFlipPreviewRatio(stage);\n                stage.innerHTML = '<div class=\"hint\">No front side selected<\/div>';\n                return;\n            }\n\n            syncFlipPreviewRatio(stage, activeSrc);\n\n            if (stage.dataset.frontSrc === frontSrc && stage.dataset.backSrc === backSrc && stage.querySelector('.preview-flip-card')) {\n                return;\n            }\n\n            stage.dataset.frontSrc = frontSrc;\n            stage.dataset.backSrc = backSrc;\n\n            if (canFlip) {\n                stage.innerHTML =\n                    '<div class=\"preview-flip-stage-inner\">' +\n                    '<div class=\"preview-flip-card\">' +\n                    '<div class=\"preview-flip-face-panel is-front\"><img decoding=\"async\" src=\"' + escapeAttr(frontSrc) + '\" alt=\"Front side\"><\/div>' +\n                    '<div class=\"preview-flip-face-panel is-back\"><img decoding=\"async\" src=\"' + escapeAttr(backSrc) + '\" alt=\"Back side\"><\/div>' +\n                    '<\/div>' +\n                    '<\/div>';\n                return;\n            }\n\n            stage.innerHTML =\n                '<div class=\"preview-flip-stage-inner\">' +\n                '<div class=\"preview-flip-card\">' +\n                '<div class=\"preview-flip-face-panel is-front\"><img decoding=\"async\" src=\"' + escapeAttr(activeSrc) + '\" alt=\"preview\"><\/div>' +\n                '<\/div>' +\n                '<\/div>';\n        }\n\n        function toggleFlipPreviewFace() {\n            const wrap = document.getElementById('preview-flip-wrap');\n            const previewBefore = document.getElementById('preview-before');\n            const previewAfter = document.getElementById('preview-after');\n            if (!wrap || !previewBefore || !previewAfter) return;\n\n            const hasFront = !!previewBefore.querySelector('img');\n            const hasBack = !!previewAfter.querySelector('img');\n            if (!(hasFront && hasBack)) return;\n\n            wrap.dataset.face = (wrap.dataset.face === 'back') ? 'front' : 'back';\n            syncFlipPreview();\n        }\n\n        function initFlipPreviewSync() {\n            const stage = document.getElementById('preview-flip-stage');\n            const btn = document.getElementById('preview-flip-btn');\n            const previewBefore = document.getElementById('preview-before');\n            const previewAfter = document.getElementById('preview-after');\n\n            if (stage) {\n                stage.addEventListener('click', toggleFlipPreviewFace);\n            }\n            if (btn) {\n                btn.addEventListener('click', function(e) {\n                    e.preventDefault();\n                    toggleFlipPreviewFace();\n                });\n            }\n\n            if (previewBefore && previewAfter && window.MutationObserver) {\n                const observer = new MutationObserver(() => {\n                    syncFlipPreview();\n                    syncSubmitButtonVisibility();\n                });\n                observer.observe(previewBefore, {\n                    childList: true,\n                    subtree: true\n                });\n                observer.observe(previewAfter, {\n                    childList: true,\n                    subtree: true\n                });\n            }\n            syncFlipPreview();\n        }\n\n        if (document.readyState === 'loading') {\n            document.addEventListener('DOMContentLoaded', function() {\n                initFlipPreviewSync();\n                syncSubmitButtonVisibility();\n            });\n        } else {\n            initFlipPreviewSync();\n            syncSubmitButtonVisibility();\n        }\n\n        function getCardElById(recId) {\n            return pickGallery.querySelector(`.card[data-id=\"${CSS.escape(recId)}\"]`);\n        }\n\n        function clearCardUI(card) {\n            if (!card) return;\n            \/\/ reset l\u00e1\u00bb\u203ap\n            card.classList.remove('is-front', 'is-back');\n\n            const btnF = card.querySelector('[data-act=\"front\"]');\n            const btnB = card.querySelector('[data-act=\"back\"]');\n            btnF && btnF.classList.remove('is-active');\n            btnB && btnB.classList.remove('is-active');\n\n            \/\/ reset icon v\u00e1\u00bb\u0081 m\u00e1\u00ba\u00b7c \u00c4\u2018\u00e1\u00bb\u2039nh\n            const imgF = btnF?.querySelector('img');\n            const imgB = btnB?.querySelector('img');\n            if (imgF) imgF.src = ICON_DEFAULT;\n            if (imgB) imgB.src = ICON_DEFAULT;\n\n            const tag = card.querySelector('.tag');\n            if (tag) tag.remove();\n        }\n\n        function setCardUI(card, role) {\n            if (!card) return;\n\n            \/\/ x\u00c3\u00b3a tr\u00e1\u00ba\u00a1ng th\u00c3\u00a1i c\u00c5\u00a9 + icon c\u00c5\u00a9\n            clearCardUI(card);\n\n            \/\/ set tr\u00e1\u00ba\u00a1ng th\u00c3\u00a1i m\u00e1\u00bb\u203ai\n            card.classList.add(role === 'front' ? 'is-front' : 'is-back');\n\n            const btn = card.querySelector(`[data-act=\"${role}\"]`);\n            if (btn) {\n                btn.classList.add('is-active');\n                const img = btn.querySelector('img');\n                if (img) img.src = ICON_ACTIVE; \/\/ \u00c4\u2018\u00e1\u00bb\u2022i sang namecard2.png khi ACTIVE\n            }\n        }\n\n\n\n\n\n\n\n        \/\/ \u00c4\u0090\u00e1\u00ba\u00a3m b\u00e1\u00ba\u00a3o: 1) m\u00e1\u00bb\u2014i role (front\/back) ch\u00e1\u00bb\u2030 c\u00c3\u00b3 1 card tr\u00c3\u00aan to\u00c3\u00a0n h\u00e1\u00bb\u2021 th\u00e1\u00bb\u2018ng\n        \/\/          2) m\u00e1\u00bb\u2014i card ch\u00e1\u00bb\u2030 gi\u00e1\u00bb\u00af 1 role duy nh\u00e1\u00ba\u00a5t t\u00e1\u00ba\u00a1i 1 th\u00e1\u00bb\u009di \u00c4\u2018i\u00e1\u00bb\u0192m\n        function applySelection(role, recId) {\n            const otherRole = role === 'front' ? 'back' : 'front';\n\n            \/\/ N\u00e1\u00ba\u00bfu \u00c4\u2018ang c\u00c3\u00b3 card kh\u00c3\u00a1c gi\u00e1\u00bb\u00af c\u00c3\u00b9ng role -> g\u00e1\u00bb\u00a1\n            if (role === 'front' && selectedFrontId && selectedFrontId !== recId) {\n                clearCardUI(getCardElById(selectedFrontId));\n            }\n            if (role === 'back' && selectedBackId && selectedBackId !== recId) {\n                clearCardUI(getCardElById(selectedBackId));\n            }\n\n            \/\/ N\u00e1\u00ba\u00bfu card n\u00c3\u00a0y \u00c4\u2018ang gi\u00e1\u00bb\u00af role c\u00c3\u00b2n l\u00e1\u00ba\u00a1i -> g\u00e1\u00bb\u00a1 role c\u00c3\u00b2n l\u00e1\u00ba\u00a1i\n            if (selectedFrontId === recId && role === 'back') {\n                clearCardUI(getCardElById(selectedFrontId));\n                selectedFrontId = null;\n            }\n            if (selectedBackId === recId && role === 'front') {\n                clearCardUI(getCardElById(selectedBackId));\n                selectedBackId = null;\n            }\n\n            \/\/ Set selection m\u00e1\u00bb\u203ai\n            const card = getCardElById(recId);\n            setCardUI(card, role);\n\n            if (role === 'front') selectedFrontId = recId;\n            else selectedBackId = recId;\n            syncSubmitButtonVisibility();\n        }\n\n\n        \/\/ X\u00c3\u00b3a item trong GRID (Stage A) theo id (tr\u00c3\u00b9ng v\u00e1\u00bb\u203ai rec.id \/ data-pid)\n        function removeGridItemById(recId) {\n            if (!recId) return;\n            \/\/ T\u00c3\u00acm .item trong #grid c\u00c3\u00b3 data-pid = recId\n            const sel = `#grid .item[data-pid=\"${CSS.escape(recId)}\"]`;\n            const gridItem = document.querySelector(sel);\n            if (!gridItem) return;\n\n            \/\/ Gi\u00e1\u00ba\u00a3i ph\u00c3\u00b3ng URL blob n\u00e1\u00ba\u00bfu c\u00c3\u00b3\n            const u = gridItem.dataset.blobUrl;\n            if (u) {\n                try {\n                    URL.revokeObjectURL(u);\n                } catch (_) {}\n            }\n\n            \/\/ G\u00e1\u00bb\u00a1 marker\/hints n\u00e1\u00ba\u00bfu \u00c4\u2018ang hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039\n            gridItem.querySelectorAll('.marker').forEach(m => m.remove());\n            const help = gridItem.querySelector('.overlay .help');\n            if (help) help.style.display = 'none';\n\n            \/\/ X\u00c3\u00b3a kh\u00e1\u00bb\u008fi DOM\n            gridItem.remove();\n\n            \/\/ \u00c4\u0090\u00e1\u00bb\u201cng b\u00e1\u00bb\u2122 l\u00e1\u00ba\u00a1i \u00c4\u2018\u00e1\u00ba\u00bfm\/b\u00e1\u00bb\u2122 nh\u00e1\u00bb\u203a\n            if (typeof updateBulkState === 'function') updateBulkState();\n        }\n    <\/script>\n\n    <!-- ========== SCRIPT (Merged logic) ========== -->\n    <script>\n        \/\/ ===== Utility for stepper state =====\n        const stageA = document.getElementById('stage-a');\n        const stageB = document.getElementById('stage-b');\n        const dotA = document.getElementById('dot-a');\n        const dotB = document.getElementById('dot-b');\n        const btnNext = document.getElementById('go-next');\n        const btnPrev = document.getElementById('go-prev');\n        const countFixed = document.getElementById('count-fixed');\n        const stepper = document.querySelector('.stepper');\n        const grid = document.getElementById(\"grid\");\n\n        function setStep(step) {\n            if (step === 1) {\n                stageA.classList.add('active');\n                stageB.classList.remove('active');\n                dotA.classList.add('active');\n                dotB.classList.remove('active');\n\n                \/\/ Quay l\u00e1\u00ba\u00a1i \u00e1\u00bb\u0178 Step 1: \u00e1\u00ba\u00a9n h\u00e1\u00ba\u00b3n n\u00c3\u00bat + disable\n                btnPrev.hidden = true;\n                btnPrev.disabled = true;\n\n                \/\/ Next ch\u00e1\u00bb\u2030 b\u00e1\u00ba\u00adt khi c\u00c3\u00b3 \u00e1\u00ba\u00a3nh \u00c4\u2018\u00c3\u00a3 x\u00e1\u00bb\u00ad l\u00c3\u00bd\n                btnNext.disabled = processedStore.length === 0;\n            } else {\n                stageA.classList.remove('active');\n                stageB.classList.add('active');\n                dotA.classList.remove('active');\n                dotB.classList.add('active');\n\n                \/\/ Sang Step 2: hi\u00e1\u00bb\u2021n l\u00e1\u00ba\u00a1i n\u00c3\u00bat Quay l\u00e1\u00ba\u00a1i\n                btnPrev.hidden = false;\n                btnPrev.disabled = false;\n\n                \/\/ \u00e1\u00bb\u017e B ch\u00e1\u00bb\u2030 c\u00c3\u00b3 submit\n                btnNext.disabled = true;\n            }\n        }\n\n        btnPrev.addEventListener('click', () => setStep(1));\n\n        \/\/ \u00e1\u00ba\u00a8n\/hi\u00e1\u00bb\u2021n stepper theo s\u00e1\u00bb\u2018 l\u00c6\u00b0\u00e1\u00bb\u00a3ng item trong grid-camera\n        function updateStepperVisibility() {\n            const hasItems = !!document.querySelector('#grid.grid-camera .item');\n            \/\/ d\u00c3\u00b9ng hidden \u00c4\u2018\u00e1\u00bb\u0192 kh\u00c3\u00b4ng ph\u00c3\u00a1 layout; c\u00c3\u00b3 th\u00e1\u00bb\u0192 \u00c4\u2018\u00e1\u00bb\u2022i sang style.display n\u00e1\u00ba\u00bfu b\u00e1\u00ba\u00a1n mu\u00e1\u00bb\u2018n\n            stepper.hidden = !hasItems;\n        }\n\n        \/\/ Theo d\u00c3\u00b5i thay \u00c4\u2018\u00e1\u00bb\u2022i con tr\u00e1\u00bb\u00b1c ti\u00e1\u00ba\u00bfp trong #grid (th\u00c3\u00aam\/x\u00c3\u00b3a item)\n        const gridObserver = new MutationObserver(() => {\n            updateStepperVisibility();\n        });\n        gridObserver.observe(grid, {\n            childList: true\n        });\n\n        function getStickyOffset() {\n            const adminbar = document.getElementById('wpadminbar');\n            const abH = adminbar ? adminbar.offsetHeight : 0;\n            const headerB = document.querySelector('#stage-b header.sticky');\n            const hH = headerB ? headerB.offsetHeight : 0;\n            return abH + hH + 12; \/\/ +12px cho kho\u00e1\u00ba\u00a3ng th\u00e1\u00bb\u0178\n        }\n\n        function scrollToPickGallery() {\n            const wrap = document.getElementById('pick-gallery-item');\n            if (!wrap) return;\n            const y = wrap.getBoundingClientRect().top + window.pageYOffset - getStickyOffset();\n            window.scrollTo({\n                top: y,\n                behavior: 'smooth'\n            });\n        }\n        btnNext.addEventListener('click', () => {\n            \/\/ build gallery-item from processedStore\n            buildPickGallery();\n            setStep(2);\n            requestAnimationFrame(() => {\n                \/\/ \u00c4\u2018\u00c3\u00b4i khi \u00e1\u00ba\u00a3nh blob load t\u00e1\u00ba\u00a1o ra reflow: k\u00c3\u00a9o l\u00e1\u00ba\u00a7n n\u00e1\u00bb\u00afa cho ch\u00e1\u00ba\u00afc\n                scrollToPickGallery();\n                setTimeout(scrollToPickGallery, 50);\n            });\n            const firstFrontBtn = document.querySelector('#pick-gallery-item [data-act=\"front\"]');\n            if (firstFrontBtn) firstFrontBtn.focus({\n                preventScroll: true\n            });\n        });\n\n        \/\/ ====== A's logic (trimmed to the essentials + unchanged algorithms) ======\n\n        const uploadInputs = Array.from(document.querySelectorAll('.a-input[type=\"file\"]'));\n        \/\/ ==== Multi-capture Camera ====\n        let camStream = null;\n        let shots = []; \/\/ {blob, filename, url}\n\n        \/\/ When set, accepts from main camera will replace this card instead of adding new processed items\n        let updateTarget = null; \/\/ { id, rec, card } or null\n\n        const FORCE_MIRROR = null; \/\/ true\/false \u00c4\u2018\u00e1\u00bb\u0192 \u00c3\u00a9p, null = t\u00e1\u00bb\u00b1 \u00c4\u2018\u00e1\u00bb\u2122ng\n        let camMirror = false;\n        let camOrientMode = 'landscape'; \/\/ 'landscape' | 'portrait' (b\u00e1\u00bb\u008f 'auto')\n        let camLastAutoOrient = 'landscape'; \/\/ {blob, filename, url}\n\n        const startCamBtn = document.getElementById('start-camera');\n        const camWrap = document.getElementById('camera-container');\n        const camVideo = document.getElementById('cam-video');\n        const camGuideBox = document.getElementById('cam-guide-box');\n        const camToggleOrient = document.getElementById('cam-toggle-orient');\n        const camThumbs = document.getElementById('cam-thumbs');\n        const camCapture = document.getElementById('cam-capture');\n        const camDone = document.getElementById('cam-done');\n        const camCancel = document.getElementById('cam-cancel');\n        const camCount = document.getElementById('cam-count');\n\n        const processedStore = []; \/\/ { id, name, getBlob:()=>Promise<Blob>, getFile:()=>Promise<File> }\n\n        function clamp(n, min, max) {\n            return Math.max(min, Math.min(max, n));\n        }\n\n        function chooseAutoOrient() {\n            if (camVideo && camVideo.videoWidth && camVideo.videoHeight) {\n                return (camVideo.videoWidth >= camVideo.videoHeight) ? 'landscape' : 'portrait';\n            }\n            const r = camVideo ? camVideo.getBoundingClientRect() : {\n                width: 1,\n                height: 1\n            };\n            return (r.width >= r.height) ? 'landscape' : 'portrait';\n        }\n\n        function updateOrientButton() {\n            if (!camToggleOrient) return;\n            const label = (camOrientMode === 'landscape') ? 'Horizontal' : 'Vertical';\n            camToggleOrient.textContent = label;\n        }\n\n        function cycleOrientMode() {\n            \/\/ Ch\u00e1\u00bb\u2030 toggle gi\u00e1\u00bb\u00afa landscape (NGANG) v\u00c3\u00a0 portrait (D\u00e1\u00bb\u0152C)\n            camOrientMode = (camOrientMode === 'landscape') ? 'portrait' : 'landscape';\n            updateOrientButton();\n            updateGuideBoxSize();\n            const heightCamEl = document.querySelector('#camera-container .height-cam');\n            if (heightCamEl) heightCamEl.classList.toggle('is-portrait', camOrientMode === 'portrait');\n        }\n\n        function detectMirrorFromStream(stream) {\n            try {\n                const track = stream && stream.getVideoTracks ? stream.getVideoTracks()[0] : null;\n                const settings = track && track.getSettings ? track.getSettings() : {};\n                const facing = (settings.facingMode || '').toLowerCase();\n                \/\/ camera tr\u00c6\u00b0\u00e1\u00bb\u203ac th\u00c6\u00b0\u00e1\u00bb\u009dng b\u00e1\u00bb\u2039 mirror; camera sau th\u00c3\u00ac kh\u00c3\u00b4ng\n                return (facing === 'user');\n            } catch (e) {\n                return false;\n            }\n        }\n\n        function applyMirrorClass() {\n            if (!camWrap) return;\n            camWrap.classList.toggle('is-mirror', !!camMirror);\n        }\n\n        function getDesiredOrient() {\n            if (camOrientMode === 'landscape' || camOrientMode === 'portrait') return camOrientMode;\n            return camLastAutoOrient || chooseAutoOrient();\n        }\n\n        function updateGuideBoxSize(forceOrient) {\n            if (!camGuideBox || !camVideo) return;\n            const rect = camVideo.getBoundingClientRect();\n            const orient = forceOrient || getDesiredOrient();\n\n            \/\/ d\u00c3\u00b9ng \u00c4\u2018\u00c3\u00bang t\u00e1\u00bb\u2030 l\u00e1\u00bb\u2021 namecard (px) \u00c4\u2018\u00e1\u00bb\u0192 tr\u00c3\u00a1nh m\u00c3\u00a9o \u00e1\u00ba\u00a3nh\n            const ratio = (orient === 'portrait') ? (CARD_W_PORTRAIT \/ CARD_H_PORTRAIT) : (CARD_W_LANDSCAPE \/\n                CARD_H_LANDSCAPE);\n\n            const maxW = rect.width * 0.75;\n            const maxH = rect.height * 0.75;\n\n            let w = maxW;\n            let h = w \/ ratio;\n            if (h > maxH) {\n                h = maxH;\n                w = h * ratio;\n            }\n\n            camGuideBox.style.width = w + 'px';\n            camGuideBox.style.height = h + 'px';\n            camGuideBox.style.aspectRatio = 'auto';\n        }\n\n        function getVideoCoverMetrics() {\n            const vw = camVideo.videoWidth || 0;\n            const vh = camVideo.videoHeight || 0;\n            const rect = camVideo.getBoundingClientRect();\n            const cw = rect.width || 1;\n            const ch = rect.height || 1;\n            if (!vw || !vh) {\n                return {\n                    vw: 1,\n                    vh: 1,\n                    cw,\n                    ch,\n                    scale: 1,\n                    offsetX: 0,\n                    offsetY: 0\n                };\n            }\n            \/\/ object-fit: cover\n            const scale = Math.max(cw \/ vw, ch \/ vh);\n            const drawnW = vw * scale;\n            const drawnH = vh * scale;\n            const offsetX = (cw - drawnW) \/ 2;\n            const offsetY = (ch - drawnH) \/ 2;\n            return {\n                vw,\n                vh,\n                cw,\n                ch,\n                scale,\n                offsetX,\n                offsetY\n            };\n        }\n\n        function getGuideCropRectInVideoPx() {\n            if (!camGuideBox || !camVideo) return null;\n            const vb = camVideo.getBoundingClientRect();\n            const gb = camGuideBox.getBoundingClientRect();\n            const m = getVideoCoverMetrics();\n\n            \/\/ v\u00e1\u00bb\u2039 tr\u00c3\u00ad c\u00e1\u00bb\u00a7a guide box trong \"khung hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039\" (CSS px)\n            const xOnCanvas = (gb.left - vb.left) - m.offsetX;\n            const yOnCanvas = (gb.top - vb.top) - m.offsetY;\n\n            let x = xOnCanvas \/ m.scale;\n            let y = yOnCanvas \/ m.scale;\n            let w = gb.width \/ m.scale;\n            let h = gb.height \/ m.scale;\n\n            \/\/ clamp trong frame video th\u00e1\u00bb\u00b1c (video px)\n            x = clamp(x, 0, m.vw);\n            y = clamp(y, 0, m.vh);\n            w = clamp(w, 1, m.vw - x);\n            h = clamp(h, 1, m.vh - y);\n\n            return {\n                x,\n                y,\n                w,\n                h,\n                vw: m.vw,\n                vh: m.vh\n            };\n        }\n\n        \/\/ c\u00e1\u00ba\u00adp nh\u00e1\u00ba\u00adt guide khi thay \u00c4\u2018\u00e1\u00bb\u2022i k\u00c3\u00adch th\u00c6\u00b0\u00e1\u00bb\u203ac\n        window.addEventListener('resize', () => updateGuideBoxSize(), {\n            passive: true\n        });\n        window.addEventListener('orientationchange', () => setTimeout(() => updateGuideBoxSize(), 180), {\n            passive: true\n        });\n\n        if (camToggleOrient) {\n            camToggleOrient.addEventListener('click', cycleOrientMode);\n            updateOrientButton();\n        }\n\n        \/\/ --- HELPER: T\u00c3\u0152M CAMERA CH\u00c3\u008dNH (1x) CHO MOBILE ---\n        async function getBestBackCameraId() {\n            if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return null;\n\n            let devices = await navigator.mediaDevices.enumerateDevices();\n\n            \/\/ Y\u00c3\u00aau c\u00e1\u00ba\u00a7u quy\u00e1\u00bb\u0081n n\u00e1\u00ba\u00bfu nh\u00c3\u00a3n tr\u00e1\u00bb\u2018ng\n            if (devices.every(d => !d.label)) {\n                try {\n                    const stream = await navigator.mediaDevices.getUserMedia({\n                        video: true\n                    });\n                    stream.getTracks().forEach(t => t.stop());\n                    devices = await navigator.mediaDevices.enumerateDevices();\n                } catch (e) {\n                }\n            }\n\n            const videoDevices = devices.filter(device => device.kind === 'videoinput');\n\n            \/\/ L\u00e1\u00bb\u008dc camera sau\n            const backCameras = videoDevices.filter(device => {\n                const label = device.label.toLowerCase();\n                return label.includes('back') || label.includes('rear') || label.includes('sau') || label.includes('environment');\n            });\n\n            if (backCameras.length > 1) {\n                \/\/ 1. \u00c6\u00afu ti\u00c3\u00aan camera c\u00c3\u00b3 nh\u00c3\u00a3n \"0\" ho\u00e1\u00ba\u00b7c \"main\" ho\u00e1\u00ba\u00b7c \"wide-angle\" (nh\u00c6\u00b0ng kh\u00c3\u00b4ng ph\u00e1\u00ba\u00a3i ultra)\n                let bestCam = backCameras.find(device => {\n                    const label = device.label.toLowerCase();\n                    return (label.includes('camera 0') || label.includes('main')) && !label.includes('ultra');\n                });\n\n                \/\/ 2. N\u00e1\u00ba\u00bfu kh\u00c3\u00b4ng t\u00c3\u00acm th\u00e1\u00ba\u00a5y theo t\u00c3\u00aan c\u00e1\u00bb\u00a5 th\u00e1\u00bb\u0192, l\u00e1\u00bb\u008dc camera 1x chu\u00e1\u00ba\u00a9n\n                if (!bestCam) {\n                    bestCam = backCameras.find(device => {\n                        const label = device.label.toLowerCase();\n                        const isUltra = label.includes('ultra') || label.includes('0.5') || label.includes('g\u00c3\u00b3c r\u00e1\u00bb\u2122ng') || label.includes('wide-angle');\n                        \/\/ L\u00c6\u00b0u \u00c3\u00bd: Tr\u00c3\u00aan Samsung, 'wide-angle' l\u00c3\u00a0 cam ch\u00c3\u00adnh, 'ultra wide-angle' l\u00c3\u00a0 cam 0.5.\n                        \/\/ Ta c\u00e1\u00ba\u00a7n t\u00c3\u00acm cam c\u00c3\u00b3 'wide' nh\u00c6\u00b0ng KH\u00c3\u201dNG c\u00c3\u00b3 'ultra'\n                        return label.includes('wide') && !label.includes('ultra');\n                    });\n                }\n\n                \/\/ 3. Fallback: N\u00e1\u00ba\u00bfu v\u00e1\u00ba\u00abn kh\u00c3\u00b4ng t\u00c3\u00acm \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c, ch\u00e1\u00bb\u008dn camera \u00c4\u2018\u00e1\u00ba\u00a7u ti\u00c3\u00aan KH\u00c3\u201dNG ph\u00e1\u00ba\u00a3i l\u00c3\u00a0 cam Ultra\n                if (!bestCam) {\n                    bestCam = backCameras.find(device => !device.label.toLowerCase().includes('ultra'));\n                }\n\n                if (bestCam) return bestCam.deviceId;\n            }\n\n            \/\/ N\u00e1\u00ba\u00bfu ch\u00e1\u00bb\u2030 c\u00c3\u00b3 1 camera sau, tr\u00e1\u00ba\u00a3 v\u00e1\u00bb\u0081 ID \u00c4\u2018\u00c3\u00b3 ho\u00e1\u00ba\u00b7c tr\u00e1\u00ba\u00a3 v\u00e1\u00bb\u0081 null \u00c4\u2018\u00e1\u00bb\u0192 d\u00c3\u00b9ng facingMode m\u00e1\u00ba\u00b7c \u00c4\u2018\u00e1\u00bb\u2039nh\n            return backCameras.length > 0 ? backCameras[0].deviceId : null;\n        }\n\n        async function startCamera() {\n            \/\/ n\u00e1\u00ba\u00bfu \u00c4\u2018ang m\u00e1\u00bb\u0178 r\u00e1\u00bb\u201ci => toggle hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039\n            if (camWrap.style.display === 'block') {\n                stopCamera();\n                return;\n            }\n\n            \/\/ iOS \/ in-app c\u00c3\u00b3 th\u00e1\u00bb\u0192 kh\u00c3\u00b4ng h\u00e1\u00bb\u2014 tr\u00e1\u00bb\u00a3 getUserMedia => fallback\n            if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n                alert('Thi\u00e1\u00ba\u00bft b\u00e1\u00bb\u2039 kh\u00c3\u00b4ng h\u00e1\u00bb\u2014 tr\u00e1\u00bb\u00a3 m\u00e1\u00bb\u0178 camera tr\u00e1\u00bb\u00b1c ti\u00e1\u00ba\u00bfp. Vui l\u00c3\u00b2ng d\u00c3\u00b9ng n\u00c3\u00bat Ch\u00e1\u00bb\u008dn file (Album).');\n                return;\n            }\n\n            try {\n                const bestBackId = await getBestBackCameraId();\n\n                const videoConstraints = {\n                    facingMode: 'environment',\n                    \/\/ \u00c3\u2030p thi\u00e1\u00ba\u00bft b\u00e1\u00bb\u2039 ph\u00e1\u00ba\u00a3i \u00c4\u2018\u00e1\u00ba\u00a1t t\u00e1\u00bb\u2018i thi\u00e1\u00bb\u0192u HD v\u00c3\u00a0 \u00c6\u00b0u ti\u00c3\u00aan Full HD\n                    width: {\n                        min: 1280,\n                        ideal: 1920,\n                        max: 4096\n                    },\n                    height: {\n                        min: 720,\n                        ideal: 1080,\n                        max: 2160\n                    },\n                    \/\/ Th\u00c3\u00aam t\u00e1\u00bb\u2030 l\u00e1\u00bb\u2021 khung h\u00c3\u00acnh \u00c4\u2018\u00e1\u00bb\u0192 tr\u00c3\u00a1nh b\u00e1\u00bb\u2039 m\u00c3\u00a9o\/m\u00e1\u00bb\u009d do scale\n                    aspectRatio: {\n                        ideal: 1.7777777778\n                    }\n                };\n\n                \/\/ N\u00e1\u00ba\u00bfu t\u00c3\u00acm th\u00e1\u00ba\u00a5y Device ID c\u00e1\u00bb\u00a7a camera ch\u00c3\u00adnh, \u00c6\u00b0u ti\u00c3\u00aan d\u00c3\u00b9ng n\u00c3\u00b3\n                if (bestBackId) {\n                    delete videoConstraints.facingMode;\n                    videoConstraints.deviceId = {\n                        exact: bestBackId\n                    };\n                }\n\n                camStream = await navigator.mediaDevices.getUserMedia({\n                    video: videoConstraints,\n                    audio: false\n                });\n                camVideo.srcObject = camStream;\n\n                \/\/ --- T\u00e1\u00bb\u0090I \u00c6\u00afU CHO ANDROID (SAMSUNG\/XIAOMI...) ---\n                const track = camStream.getVideoTracks()[0];\n                if (track.getCapabilities && track.applyConstraints) {\n                    const capabilities = track.getCapabilities();\n                    const advConstraints = {};\n\n                    \/\/ 1. L\u00e1\u00ba\u00a5y n\u00c3\u00a9t li\u00c3\u00aan t\u00e1\u00bb\u00a5c (Quan tr\u00e1\u00bb\u008dng nh\u00e1\u00ba\u00a5t cho danh thi\u00e1\u00ba\u00bfp)\n                    if (capabilities.focusMode && capabilities.focusMode.includes('continuous')) {\n                        advConstraints.focusMode = 'continuous';\n                    }\n\n                    \/\/ 2. Ch\u00e1\u00bb\u2018ng rung\n                    if (capabilities.imageStabilizationMode && capabilities.imageStabilizationMode.includes('on')) {\n                        advConstraints.imageStabilizationMode = 'on';\n                    }\n\n                    \/\/ 3. \u00c3\u2030p Zoom v\u00e1\u00bb\u0081 1x n\u00e1\u00ba\u00bfu h\u00e1\u00bb\u2014 tr\u00e1\u00bb\u00a3 v\u00c3\u00a0 \u00c4\u2018ang b\u00e1\u00bb\u2039 0.5x\n                    if (capabilities.zoom) {\n                        \/\/ Th\u00e1\u00bb\u00ad set zoom v\u00e1\u00bb\u0081 1.0 ho\u00e1\u00ba\u00b7c gi\u00c3\u00a1 tr\u00e1\u00bb\u2039 t\u00e1\u00bb\u2018i thi\u00e1\u00bb\u0192u c\u00e1\u00bb\u00a7a m\u00c3\u00a1y n\u00e1\u00ba\u00bfu m\u00c3\u00a1y b\u00e1\u00ba\u00aft \u00c4\u2018\u00e1\u00ba\u00a7u t\u00e1\u00bb\u00ab 0.5\n                        advConstraints.zoom = capabilities.zoom.min > 1 ? capabilities.zoom.min : 1;\n                    }\n\n                    if (Object.keys(advConstraints).length > 0) {\n                        await track.applyConstraints({\n                            advanced: [advConstraints]\n                        });\n                    }\n                }\n\n                \/\/ mirror preview (ch\u00e1\u00bb\u00a7 y\u00e1\u00ba\u00bfu v\u00e1\u00bb\u203ai camera tr\u00c6\u00b0\u00e1\u00bb\u203ac)\n                camMirror = (FORCE_MIRROR === true) ? true : (FORCE_MIRROR === false ? false : detectMirrorFromStream(\n                    camStream));\n                applyMirrorClass();\n\n                \/\/ reset ch\u00e1\u00ba\u00bf \u00c4\u2018\u00e1\u00bb\u2122 khung ch\u00e1\u00bb\u00a5p\n                \/\/ reset v\u00e1\u00bb\u0081 landscape khi m\u00e1\u00bb\u0178 l\u00e1\u00ba\u00a1i camera\n                camOrientMode = 'landscape';\n                camLastAutoOrient = chooseAutoOrient();\n                updateOrientButton();\n\n                \/\/ khi video s\u00e1\u00ba\u00b5n s\u00c3\u00a0ng => c\u00e1\u00ba\u00adp nh\u00e1\u00ba\u00adt guide box theo \u00c4\u2018\u00c3\u00bang k\u00c3\u00adch th\u00c6\u00b0\u00e1\u00bb\u203ac hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039\n                camVideo.addEventListener('loadedmetadata', () => {\n                    camLastAutoOrient = chooseAutoOrient();\n                    updateGuideBoxSize();\n                }, {\n                    once: true\n                });\n\n                shots = [];\n                refreshThumbs();\n                camWrap.style.display = 'block';\n                if (camToggleOrient) camToggleOrient.disabled = false;\n                setTimeout(() => updateGuideBoxSize(), 80);\n                setTimeout(() => updateGuideBoxSize(), 260);\n                \/\/ Scroll \u00c4\u2018\u00e1\u00bb\u0192 v\u00c3\u00b9ng camera (bao g\u00e1\u00bb\u201cm n\u00c3\u00bat Ch\u00e1\u00bb\u00a5p\/NGANG\/D\u00c3\u00b9ng X \u00e1\u00ba\u00a3nh) hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039 \u00c4\u2018\u00c3\u00bang\n                setTimeout(() => {\n                    const camControls = camWrap.querySelector('.cam-controls');\n                    const scrollTarget = camControls || camWrap;\n                    if (scrollTarget) {\n                        const rect = scrollTarget.getBoundingClientRect();\n                        const stickyHeight = document.querySelector('.sticky') ? document.querySelector('.sticky').offsetHeight : 0;\n                        const targetTop = window.scrollY + rect.bottom - window.innerHeight + 20;\n                        if (rect.bottom > window.innerHeight - 20) {\n                            window.scrollTo({ top: targetTop, behavior: 'smooth' });\n                        } else {\n                            camWrap.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n                        }\n                    }\n                }, 350);\n            } catch (e) {\n                console.error(e);\n                alert('Kh\u00c3\u00b4ng th\u00e1\u00bb\u0192 m\u00e1\u00bb\u0178 camera. H\u00c3\u00a3y ki\u00e1\u00bb\u0192m tra quy\u00e1\u00bb\u0081n truy c\u00e1\u00ba\u00adp ho\u00e1\u00ba\u00b7c d\u00c3\u00b9ng n\u00c3\u00bat Ch\u00e1\u00bb\u008dn file.');\n            }\n        }\n\n        function stopCamera() {\n            try {\n                camVideo.pause();\n            } catch (_) {}\n            if (camStream) {\n                camStream.getTracks().forEach(t => t.stop());\n                camStream = null;\n            }\n            camMirror = false;\n            applyMirrorClass();\n            if (camToggleOrient) camToggleOrient.disabled = true;\n            camWrap.style.display = 'none';\n            shots.forEach(s => {\n                if (s.url) URL.revokeObjectURL(s.url);\n            });\n            shots = [];\n            refreshThumbs();\n\n            \/\/ If we were in updateTarget mode, clear highlight and reset\n            try {\n                if (updateTarget && updateTarget.card) updateTarget.card.classList.remove('camera-target');\n            } catch (e) {}\n            updateTarget = null;\n        }\n\n        function refreshThumbs() {\n            camThumbs.innerHTML = '';\n            camCount.textContent = String(shots.length);\n            camDone.disabled = shots.length === 0;\n\n            shots.forEach((s, i) => {\n                const t = document.createElement('div');\n                t.className = 't';\n\n                const img = document.createElement('img');\n                img.src = s.url;\n                img.alt = s.filename;\n\n                const rm = document.createElement('button');\n                rm.className = 'rm';\n                rm.title = 'X\u00c3\u00b3a \u00e1\u00ba\u00a3nh n\u00c3\u00a0y';\n\n                \/\/ Thay textContent b\u00e1\u00ba\u00b1ng innerHTML \u00c4\u2018\u00e1\u00bb\u0192 d\u00c3\u00b9ng icon\n                rm.innerHTML =\n                    `<img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/cancel2.png\" alt=\"X\u00c3\u00b3a\" style=\"width:12px; height:12px;\">`;\n\n                rm.onclick = () => {\n                    if (s.url) URL.revokeObjectURL(s.url);\n                    shots.splice(i, 1);\n                    refreshThumbs();\n                };\n\n                t.appendChild(img);\n                t.appendChild(rm);\n                camThumbs.appendChild(t);\n            });\n        }\n\n        function autoCropFromVideoFrame(videoEl = camVideo) {\n            if (!cvReady || typeof cv === 'undefined' || !videoEl || !videoEl.videoWidth || !videoEl.videoHeight)\n                return null;\n\n            \/\/ 1) l\u00e1\u00ba\u00a5y frame t\u00e1\u00bb\u00ab video -> canvas\n            const fullCanvas = document.createElement('canvas');\n            fullCanvas.width = videoEl.videoWidth;\n            fullCanvas.height = videoEl.videoHeight;\n            const fctx = fullCanvas.getContext('2d', {\n                willReadFrequently: true\n            });\n            fctx.drawImage(videoEl, 0, 0);\n\n            let full = null,\n                small = null,\n                gray = null,\n                blur = null,\n                edges = null,\n                kernel = null,\n                contours = null,\n                hierarchy = null;\n            try {\n                full = cv.imread(fullCanvas);\n\n                \/\/ 2) downscale \u00c4\u2018\u00e1\u00bb\u0192 detect nhanh\n                const maxDim = 800;\n                const scale = Math.min(1, maxDim \/ Math.max(full.cols, full.rows));\n                const newW = Math.max(1, Math.round(full.cols * scale));\n                const newH = Math.max(1, Math.round(full.rows * scale));\n                small = new cv.Mat();\n                cv.resize(full, small, new cv.Size(newW, newH), 0, 0, cv.INTER_AREA);\n\n                gray = new cv.Mat();\n                blur = new cv.Mat();\n                edges = new cv.Mat();\n                cv.cvtColor(small, gray, cv.COLOR_RGBA2GRAY);\n                cv.GaussianBlur(gray, blur, new cv.Size(5, 5), 0);\n                cv.Canny(blur, edges, 60, 160);\n\n                kernel = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(3, 3));\n                cv.dilate(edges, edges, kernel);\n\n                contours = new cv.MatVector();\n                hierarchy = new cv.Mat();\n                cv.findContours(edges, contours, hierarchy, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE);\n\n                let bestPts = null;\n                let bestScore = 0;\n\n                const imgArea = small.cols * small.rows;\n                for (let i = 0; i < contours.size(); i++) {\n                    const cnt = contours.get(i);\n                    const area = cv.contourArea(cnt);\n                    if (area < imgArea * 0.02) continue;\n\n                    \/\/ \u00c6\u00b0u ti\u00c3\u00aan quad t\u00e1\u00bb\u00ab approxPolyDP\n                    const peri = cv.arcLength(cnt, true);\n                    const approx = new cv.Mat();\n                    cv.approxPolyDP(cnt, approx, 0.02 * peri, true);\n\n                    const candidates = [];\n                    if (approx.rows === 4) {\n                        const data = approx.data32S;\n                        const pts = [{\n                                x: data[0],\n                                y: data[1]\n                            },\n                            {\n                                x: data[2],\n                                y: data[3]\n                            },\n                            {\n                                x: data[4],\n                                y: data[5]\n                            },\n                            {\n                                x: data[6],\n                                y: data[7]\n                            },\n                        ];\n                        candidates.push(pts);\n                    }\n\n                    \/\/ fallback: minAreaRect\n                    try {\n                        const rr = cv.minAreaRect(cnt);\n                        const rrPts = cv.RotatedRect.points(rr);\n                        if (rrPts && rrPts.length === 4) {\n                            candidates.push(rrPts.map(p => ({\n                                x: p.x,\n                                y: p.y\n                            })));\n                        }\n                    } catch (e) {\n                        \/* ignore *\/\n                    }\n\n                    for (const pts of candidates) {\n                        const sc = scoreQuad(pts, imgArea);\n                        if (sc > bestScore) {\n                            bestScore = sc;\n                            bestPts = pts;\n                        }\n                    }\n\n                    approx.delete();\n                }\n\n                if (!bestPts || bestScore < 0.22) {\n                    return null;\n                }\n\n                const orient = (ratioFromPts(bestPts) > 1) ? 'landscape' : 'portrait';\n\n                \/\/ scale pts v\u00e1\u00bb\u0081 full size\n                const inv = 1 \/ scale;\n                const fullPts = bestPts.map(p => ({\n                    x: p.x * inv,\n                    y: p.y * inv\n                }));\n\n                \/\/ order + expand padding + warp\n                const ordered = orderPoints(fullPts);\n                const expanded = expandPoints(ordered, full.cols, full.rows, 0.05);\n                const W = (orient === 'portrait') ? CARD_W_PORTRAIT : CARD_W_LANDSCAPE;\n                const H = (orient === 'portrait') ? CARD_H_PORTRAIT : CARD_H_LANDSCAPE;\n                const warped = fourPointTransform(full, expanded, W, H);\n                const enhanced = enhanceCard(warped);\n\n                const outCanvas = document.createElement('canvas');\n                cv.imshow(outCanvas, enhanced);\n\n                \/\/ cleanup\n                warped.delete();\n                enhanced.delete();\n\n                return {\n                    canvas: outCanvas,\n                    orientation: orient,\n                    score: bestScore\n                };\n            } catch (e) {\n                console.warn('autoCropFromVideoFrame error', e);\n                return null;\n            } finally {\n                try {\n                    full && full.delete();\n                } catch (e) {}\n                try {\n                    small && small.delete();\n                } catch (e) {}\n                try {\n                    gray && gray.delete();\n                } catch (e) {}\n                try {\n                    blur && blur.delete();\n                } catch (e) {}\n                try {\n                    edges && edges.delete();\n                } catch (e) {}\n                try {\n                    kernel && kernel.delete();\n                } catch (e) {}\n                try {\n                    contours && contours.delete();\n                } catch (e) {}\n                try {\n                    hierarchy && hierarchy.delete();\n                } catch (e) {}\n            }\n        }\n\n        async function captureShot() {\n            if (!camStream || !camVideo || !camVideo.videoWidth || !camVideo.videoHeight) return;\n\n            camCapture.disabled = true;\n            try {\n                \/\/ const shutterSound = new Audio(\n                \/\/     '\/wp-content\/plugins\/convert-img-to-text\/assets\/audio\/sound-take-photo.mp3');\n                \/\/ shutterSound.volume = 0.5; \/\/ \u00c3\u201am l\u00c6\u00b0\u00e1\u00bb\u00a3ng 50%\n                \/\/ shutterSound.play().catch(err => console.log('Kh\u00c3\u00b4ng th\u00e1\u00bb\u0192 ph\u00c3\u00a1t \u00c3\u00a2m thanh:', err));\n                let outCanvas = null;\n\n                const isMobileDevice = window.innerWidth <= 768;\n\n                if (isMobileDevice) {\n                    \/\/ MOBILE: crop theo guide box (v\u00c3\u00b9ng ch\u00e1\u00bb\u008dn tr\u00c3\u00aan m\u00c3\u00a0n h\u00c3\u00acnh)\n                    const rect = getGuideCropRectInVideoPx();\n                    if (!rect) throw new Error('Kh\u00c3\u00b4ng l\u00e1\u00ba\u00a5y \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c v\u00c3\u00b9ng crop theo khung.');\n\n                    let sx = rect.x,\n                        sy = rect.y,\n                        sw = rect.w,\n                        sh = rect.h;\n                    if (camMirror) {\n                        sx = Math.max(0, rect.vw - (sx + sw));\n                    }\n\n                    const orient = getDesiredOrient();\n                    const outW = (orient === 'portrait') ? CARD_W_PORTRAIT : CARD_W_LANDSCAPE;\n                    const outH = (orient === 'portrait') ? CARD_H_PORTRAIT : CARD_H_LANDSCAPE;\n\n                    outCanvas = document.createElement('canvas');\n                    outCanvas.width = outW;\n                    outCanvas.height = outH;\n\n                    const ctx = outCanvas.getContext('2d');\n                    ctx.drawImage(camVideo, sx, sy, sw, sh, 0, 0, outW, outH);\n                } else {\n                    \/\/ PC\/TABLET: ch\u00e1\u00bb\u00a5p to\u00c3\u00a0n b\u00e1\u00bb\u2122 video frame g\u00e1\u00bb\u2018c\n                    outCanvas = document.createElement('canvas');\n                    outCanvas.width = camVideo.videoWidth;\n                    outCanvas.height = camVideo.videoHeight;\n                    const ctx = outCanvas.getContext('2d');\n                    ctx.drawImage(camVideo, 0, 0);\n                }\n\n                \/\/ \u00f0\u0178\u201c\u00b8 PNG: Ch\u00e1\u00ba\u00a5t l\u00c6\u00b0\u00e1\u00bb\u00a3ng 100% (lossless), hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039 gi\u00e1\u00bb\u2018ng \u00e1\u00ba\u00a3nh g\u00e1\u00bb\u2018c\n                const blob = await new Promise((resolve) => outCanvas.toBlob(resolve, 'image\/png'));\n                if (!blob) throw new Error('Kh\u00c3\u00b4ng t\u00e1\u00ba\u00a1o \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c \u00e1\u00ba\u00a3nh.');\n\n                const url = URL.createObjectURL(blob);\n                const filename = `camera_${Date.now()}.png`;\n\n                shots.push({\n                    blob,\n                    url,\n                    filename\n                });\n                camCount.textContent = String(shots.length);\n                refreshThumbs();\n            } catch (err) {\n                console.error(err);\n                alert('L\u00e1\u00bb\u2014i ch\u00e1\u00bb\u00a5p \u00e1\u00ba\u00a3nh: ' + (err && err.message ? err.message : err));\n            } finally {\n                camCapture.disabled = false;\n            }\n        }\n        async function acceptShots() {\n            if (!shots.length) return;\n\n            \/\/ If we are replacing a target card from stage A's camera\n            if (updateTarget) {\n                const indicator = document.getElementById('image-upload-indicator');\n                const indicatorB = document.getElementById('image-upload-indicator-b');\n                if (indicator) indicator.style.display = 'block';\n                if (indicatorB) indicatorB.style.display = 'block';\n                try {\n                    \/\/ Use the last shot as replacement\n                    const s = shots[shots.length - 1];\n                    if (s && s.blob) {\n                        await replaceCardImage(s.blob, updateTarget.rec, updateTarget.card);\n                    } else {\n                        alert('Kh\u00c3\u00b4ng c\u00c3\u00b3 \u00e1\u00ba\u00a3nh \u00c4\u2018\u00e1\u00bb\u0192 thay th\u00e1\u00ba\u00bf.');\n                    }\n                } catch (e) {\n                    console.error('Error replacing target from camera:', e);\n                    alert('Kh\u00c3\u00b4ng th\u00e1\u00bb\u0192 c\u00e1\u00ba\u00adp nh\u00e1\u00ba\u00adt \u00e1\u00ba\u00a3nh.');\n                } finally {\n                    \/\/ cleanup\n                    if (updateTarget && updateTarget.card) updateTarget.card.classList.remove('camera-target');\n                    updateTarget = null;\n                    stopCamera();\n                    if (indicator) indicator.style.display = 'none';\n                    if (indicatorB) indicatorB.style.display = 'none';\n                }\n                return;\n            }\n\n            \/\/ Hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039 loading indicator\n            const indicator = document.getElementById('image-upload-indicator');\n            const indicatorB = document.getElementById('image-upload-indicator-b');\n            if (indicator) indicator.style.display = 'block';\n            if (indicatorB) indicatorB.style.display = 'block';\n\n            \/\/ Bi\u00e1\u00ba\u00bfn blob -> File \u00c4\u2018\u00e1\u00bb\u0192 t\u00c3\u00a1i d\u00c3\u00b9ng handleFiles (\u00c4\u2018\u00c3\u00a3 h\u00e1\u00bb\u2014 tr\u00e1\u00bb\u00a3 array)\n            const files = shots.map(s => blobToFile(s.blob, s.filename) || s.blob);\n            try {\n                await handleFiles(files); \/\/ t\u00c3\u00a1i d\u00c3\u00b9ng pipeline c\u00c5\u00a9\n\n                \/\/ T\u00e1\u00bb\u00b1 \u00c4\u2018\u00e1\u00bb\u2122ng chuy\u00e1\u00bb\u0192n sang b\u00c6\u00b0\u00e1\u00bb\u203ac Pick Gallery (Stage B)\n                buildPickGallery();\n                setStep(2);\n                requestAnimationFrame(() => {\n                    scrollToPickGallery();\n                    setTimeout(scrollToPickGallery, 50);\n                });\n                const firstFrontBtn = document.querySelector('#pick-gallery-item [data-act=\"front\"]');\n                if (firstFrontBtn) firstFrontBtn.focus({\n                    preventScroll: true\n                });\n\n            } finally {\n                stopCamera(); \/\/ \u00c4\u2018\u00c3\u00b3ng camera & d\u00e1\u00bb\u008dn b\u00e1\u00bb\u2122 nh\u00e1\u00bb\u203a\n                \/\/ \u00e1\u00ba\u00a8n loading indicator sau khi x\u00e1\u00bb\u00ad l\u00c3\u00bd xong\n                if (indicator) indicator.style.display = 'none';\n                if (indicatorB) indicatorB.style.display = 'none';\n            }\n        }\n\n        \/\/ N\u00c3\u00bat & ph\u00c3\u00adm t\u00e1\u00ba\u00aft\n        if (startCamBtn) startCamBtn.addEventListener('click', startCamera);\n        if (camCapture) camCapture.addEventListener('click', captureShot);\n        if (camDone) camDone.addEventListener('click', acceptShots);\n        const startCamAddMoreBtn = document.getElementById('start-camera-add-more');\n        if (startCamAddMoreBtn) startCamAddMoreBtn.addEventListener('click', startCamera);\n        if (camCancel) camCancel.addEventListener('click', stopCamera);\n\n        \/\/ Intercept \"Chon them anh\" label to open camera modal\n        const addMoreLabel = document.querySelector(\".add-more-photos-label\");\n        if (addMoreLabel) {\n            addMoreLabel.addEventListener(\"click\", (e) => {\n                e.preventDefault();\n                startCamera();\n            });\n        }\n        \/\/ Space = ch\u00e1\u00bb\u00a5p, Enter = d\u00c3\u00b9ng\n        document.addEventListener('keydown', (e) => {\n            if (camWrap.style.display !== 'block') return;\n            if (e.code === 'Space') {\n                e.preventDefault();\n                captureShot();\n            }\n            if (e.code === 'Enter') {\n                e.preventDefault();\n                acceptShots();\n            }\n        });\n        \/\/ iOS detection\n        function isIOS() {\n            return (\/iP(hone|ad|od)\/.test(navigator.platform) || (navigator.userAgent.includes('Mac') && 'ontouchend' in\n                document));\n        }\n\n        function supportsShareFiles() {\n            return !!(navigator.share && navigator.canShare);\n        }\n\n        function isInApp() {\n            const ua = navigator.userAgent || navigator.vendor || window.opera;\n            return \/Zalo|FBAN|FBAV|Instagram|Line\\\/|KAKAOTALK|Messenger|Twitter|TikTok|Zavi\/i.test(ua);\n        }\n\n        function blobToFile(blob, filename) {\n            try {\n                return new File([blob], filename, {\n                    type: blob.type || 'image\/png'\n                })\n            } catch {\n                return null\n            }\n        }\n        async function canvasToFile(canvas, filename = 'image.png') {\n            const blob = await new Promise(res => canvas.toBlob(res, 'image\/png'));\n            const file = blobToFile(blob, filename);\n            return {\n                blob,\n                file,\n                filename\n            };\n        }\n        async function canvasesToFiles(canvases) {\n            const out = [];\n            for (let i = 0; i < canvases.length; i++) {\n                const c = canvases[i];\n                const nameEl = c.closest('.item')?.querySelector('.name');\n                const sourceName = nameEl?.textContent || ('card_' + (i + 1));\n                const dot = sourceName.lastIndexOf(\".\");\n                const base = dot >= 0 ? sourceName.slice(0, dot) : sourceName;\n                const filename = `${base}_fixed.png`;\n                out.push(await canvasToFile(c, filename));\n            }\n            return out;\n        }\n\n        async function shareFilesOrZip(filesOrBlobs, zipName = 'namecards_fixed.zip') {\n            const fileObjs = filesOrBlobs.map(x => x.file).filter(Boolean);\n            if (supportsShareFiles() && fileObjs.length && navigator.canShare({\n                    files: fileObjs\n                })) {\n                await navigator.share({\n                    files: fileObjs,\n                    title: '\u00e1\u00ba\u00a2nh \u00c4\u2018\u00c3\u00a3 x\u00e1\u00bb\u00ad l\u00c3\u00bd'\n                });\n                return;\n            }\n            if (!window.JSZip) {\n                alert('\u00c4\u0090ang t\u00e1\u00ba\u00a3i th\u00c6\u00b0 vi\u00e1\u00bb\u2021n n\u00c3\u00a9n\u00e2\u20ac\u00a6 vui l\u00c3\u00b2ng \u00c4\u2018\u00e1\u00bb\u00a3i v\u00c3\u00a0i gi\u00c3\u00a2y.');\n                return;\n            }\n            const zip = new JSZip();\n            for (const it of filesOrBlobs) {\n                const b = it.blob || it;\n                const fname = it.filename || 'image.png';\n                const ab = await b.arrayBuffer();\n                zip.file(fname, ab);\n            }\n            const zipBlob = await zip.generateAsync({\n                type: 'blob'\n            });\n            const zipFile = blobToFile(zipBlob, zipName);\n            if (supportsShareFiles() && zipFile && navigator.canShare({\n                    files: [zipFile]\n                })) {\n                await navigator.share({\n                    files: [zipFile],\n                    title: '\u00e1\u00ba\u00a2nh \u00c4\u2018\u00c3\u00a3 x\u00e1\u00bb\u00ad l\u00c3\u00bd'\n                });\n            } else {\n                await smartSaveBlob(zipBlob, zipName);\n            }\n        }\n\n        function isSafariDesktop() {\n            const ua = navigator.userAgent;\n            const isSafari = \/^((?!chrome|android|crios|fxios|edg).)*safari\/i.test(ua);\n            return isSafari && !isIOS();\n        }\n\n        function updateBulkState() {\n            \/\/ const hasItems = document.querySelector('.item canvas.out') !== null;\n\n            \/\/ if (stepper) stepper.style.display = hasItems ? '' : 'none';\n            const canvases = [...document.querySelectorAll('.item canvas.out')];\n            const disabled = canvases.length === 0;\n            btnNext.disabled = disabled;\n            countFixed.textContent = `${canvases.length} \u00e1\u00ba\u00a3nh \u00c4\u2018\u00c3\u00a3 x\u00e1\u00bb\u00ad l\u00c3\u00bd`;\n            \/\/ sync processedStore\n            const idsInDom = new Set(canvases.map(c => c.closest('.item').dataset.pid));\n            for (let i = processedStore.length - 1; i >= 0; i--) {\n                if (!idsInDom.has(processedStore[i].id)) processedStore.splice(i, 1);\n            }\n            \/\/ add any new\n            canvases.forEach(c => {\n                const item = c.closest('.item');\n                const id = item.dataset.pid;\n                if (!processedStore.find(x => x.id === id)) {\n                    const nameEl = item.querySelector('.name');\n                    const base = (nameEl?.textContent || id).replace(\/\\.[^.]+$\/, '');\n                    processedStore.push({\n                        id,\n                        name: base + \"_fixed.png\",\n                        cropMode: item.dataset.cropMode || \"\",\n                        getBlob: () => new Promise(r => c.toBlob(r, 'image\/png')),\n                        getOriginalBlob: () => item.__cittOriginalBlob ? Promise.resolve(item.__cittOriginalBlob) : new Promise(r => c.toBlob(r, 'image\/png')),\n                        getFile: async () => {\n                            const blob = await new Promise(r => c.toBlob(r, 'image\/png'));\n                            return blobToFile(blob, base + \"_fixed.png\")\n                        }\n                    });\n                }\n            });\n            updateStepperVisibility();\n        }\n\n        async function smartSaveBlob(blob, filename) {\n            if ((isIOS() || isInApp()) && supportsShareFiles()) {\n                const file = blobToFile(blob, filename) || blob;\n                try {\n                    if (file instanceof File && navigator.canShare({\n                            files: [file]\n                        })) {\n                        await navigator.share({\n                            files: [file],\n                            title: filename\n                        });\n                        return;\n                    }\n                    await shareFilesOrZip([{\n                        blob,\n                        filename\n                    }], 'namecards_fixed.zip');\n                    return;\n                } catch {\n                    return;\n                }\n            }\n            const url = URL.createObjectURL(blob);\n            const a = document.createElement('a');\n            a.href = url;\n            a.download = filename;\n            a.rel = 'noopener';\n            document.body.appendChild(a);\n            a.click();\n            setTimeout(() => {\n                a.remove();\n                URL.revokeObjectURL(url)\n            }, 1500)\n        }\n\n        if (!HTMLCanvasElement.prototype.toBlob) {\n            HTMLCanvasElement.prototype.toBlob = function(cb, type, quality) {\n                const dataURL = this.toDataURL(type, quality);\n                const bstr = atob(dataURL.split(',')[1]);\n                let n = bstr.length;\n                const u8 = new Uint8Array(n);\n                while (n--) u8[n] = bstr.charCodeAt(n);\n                cb(new Blob([u8], {\n                    type: type || 'image\/png'\n                }));\n            }\n        }\n\n        async function handleUploadInputChange(e) {\n            const targetInput = e && e.target ? e.target : null;\n            const indicator = document.getElementById('image-upload-indicator');\n            const indicatorB = document.getElementById('image-upload-indicator-b');\n            if (indicator) indicator.style.display = 'block';\n            if (indicatorB) indicatorB.style.display = 'block';\n\n            try {\n                await handleFiles(targetInput ? targetInput.files : []);\n                if (processedStore.length > 0) {\n                    setStep(2);\n                    try { buildPickGallery(); } catch (_) {}\n                    requestAnimationFrame(() => { scrollToPickGallery(); });\n                } else {\n                    setStep(1);\n                }\n            } finally {\n                if (indicator) indicator.style.display = 'none';\n                if (indicatorB) indicatorB.style.display = 'none';\n            }\n            if (targetInput) targetInput.value = '';\n        }\n\n        uploadInputs.forEach(inp => inp.addEventListener('change', handleUploadInputChange));\n\n        ['dragenter', 'dragover'].forEach(ev => document.addEventListener(ev, e => {\n            e.preventDefault();\n            e.dataTransfer.dropEffect = 'copy';\n        }));;\n        ['drop'].forEach(ev => document.addEventListener(ev, async e => {\n            e.preventDefault();\n            \/\/ Hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039 loading indicator\n            const indicator = document.getElementById('image-upload-indicator');\n            const indicatorB = document.getElementById('image-upload-indicator-b');\n            if (indicator) indicator.style.display = 'block';\n            if (indicatorB) indicatorB.style.display = 'block';\n\n            try {\n                await handleFiles(e.dataTransfer.files);\n            } finally {\n                \/\/ \u00e1\u00ba\u00a8n loading indicator sau khi x\u00e1\u00bb\u00ad l\u00c3\u00bd xong\n                if (indicator) indicator.style.display = 'none';\n                if (indicatorB) indicatorB.style.display = 'none';\n            }\n        }));\n\n        let cvReady = false;\n\n        \/\/ Initialize OpenCV - Multiple methods to ensure it works\n        window.onOpenCvReady = function() {\n            if (typeof cv !== 'undefined') {\n                cv['onRuntimeInitialized'] = () => {\n                    cvReady = true;\n                    window.dispatchEvent(new Event('opencv-ready-citt'));\n                };\n            }\n        };\n\n        \/\/ Fallback: check if cv is already loaded\n        if (typeof cv !== 'undefined' && cv.Mat) {\n            cvReady = true;\n        }\n\n        \/\/ Periodic check for OpenCV (in case onOpenCvReady is not called)\n        let checkCount = 0;\n        const checkOpenCV = setInterval(function() {\n            checkCount++;\n            if (typeof cv !== 'undefined' && cv.Mat) {\n                if (!cvReady) {\n                    cvReady = true;\n                    window.dispatchEvent(new Event('opencv-ready-citt'));\n                }\n                clearInterval(checkOpenCV);\n            } else if (checkCount >= 100) { \/\/ Stop after 10 seconds\n                console.error('\u00e2\u009d\u0152 [convert-img-to-text] OpenCV.js failed to load after 10 seconds');\n                clearInterval(checkOpenCV);\n            }\n        }, 100);\n\n        function waitForCvReady(timeoutMs = 5000) {\n            if (cvReady && typeof cv !== 'undefined' && cv.Mat) {\n                return Promise.resolve(true);\n            }\n            return new Promise((resolve) => {\n                const started = Date.now();\n                const tick = () => {\n                    if (cvReady && typeof cv !== 'undefined' && cv.Mat) {\n                        resolve(true);\n                        return;\n                    }\n                    if (Date.now() - started >= timeoutMs) {\n                        resolve(false);\n                        return;\n                    }\n                    setTimeout(tick, 120);\n                };\n                tick();\n            });\n        }\n\n        let __cittAutoCropLoadPromise = null;\n        const __cittScriptLoadPromises = Object.create(null);\n\n        function loadScriptOnce(src, attrs = {}) {\n            if (__cittScriptLoadPromises[src]) return __cittScriptLoadPromises[src];\n            __cittScriptLoadPromises[src] = new Promise((resolve, reject) => {\n                const existing = document.querySelector('script[src=\"' + src + '\"]');\n                if (existing) {\n                    resolve();\n                    return;\n                }\n                const s = document.createElement('script');\n                s.src = src;\n                s.async = true;\n                Object.keys(attrs || {}).forEach((k) => s.setAttribute(k, attrs[k]));\n                s.addEventListener('load', () => resolve(), {\n                    once: true\n                });\n                s.addEventListener('error', () => reject(new Error('Failed to load script: ' + src)), {\n                    once: true\n                });\n                document.head.appendChild(s);\n            });\n            return __cittScriptLoadPromises[src];\n        }\n\n        async function ensureNameCardAutoCropReady(timeoutMs = 6000) {\n            if (\n                window.NameCardAutoCrop &&\n                typeof window.NameCardAutoCrop.isReady === 'function' &&\n                window.NameCardAutoCrop.isReady()\n            ) {\n                return true;\n            }\n\n            if (!__cittAutoCropLoadPromise) {\n                __cittAutoCropLoadPromise = new Promise(async (resolve) => {\n                    const cvOk = await waitForCvReady(timeoutMs);\n                    if (!cvOk) {\n                        resolve(false);\n                        return;\n                    }\n                    try {\n                        await loadScriptOnce('\/wp-content\/plugins\/name-card-user\/assets\/js\/auto-crop.js', {\n                            'data-citt-autocrop': '1'\n                        });\n                    } catch (e) {\n                        console.warn('[CITT] failed loading NameCardAutoCrop script', e);\n                        resolve(false);\n                        return;\n                    }\n\n                    const started = Date.now();\n                    const tick = () => {\n                        const ready = window.NameCardAutoCrop &&\n                            typeof window.NameCardAutoCrop.isReady === 'function' &&\n                            window.NameCardAutoCrop.isReady();\n                        if (ready) {\n                            resolve(true);\n                            return;\n                        }\n                        if (Date.now() - started >= timeoutMs) {\n                            resolve(false);\n                            return;\n                        }\n                        setTimeout(tick, 120);\n                    };\n                    tick();\n                });\n            }\n\n            const ready = await __cittAutoCropLoadPromise;\n            if (!ready) __cittAutoCropLoadPromise = null;\n            return ready;\n        }\n\n        function rgbDist(r1, g1, b1, r2, g2, b2) {\n            const dr = r1 - r2;\n            const dg = g1 - g2;\n            const db = b1 - b2;\n            return Math.sqrt(dr * dr + dg * dg + db * db);\n        }\n\n        function loadImageFromFile(file) {\n            return new Promise((resolve, reject) => {\n                const url = URL.createObjectURL(file);\n                const img = new Image();\n                img.onload = () => {\n                    URL.revokeObjectURL(url);\n                    resolve(img);\n                };\n                img.onerror = (e) => {\n                    URL.revokeObjectURL(url);\n                    reject(e);\n                };\n                img.src = url;\n            });\n        }\n\n        function detectCardBoundsBasic(canvas, ctx) {\n            const w = canvas.width;\n            const h = canvas.height;\n            const imgData = ctx.getImageData(0, 0, w, h);\n            const d = imgData.data;\n\n            const band = Math.max(10, Math.floor(Math.min(w, h) * 0.06));\n            let br = 0, bg = 0, bb = 0, n = 0;\n            for (let y = 0; y < h; y++) {\n                for (let x = 0; x < w; x++) {\n                    const onBorder = x < band || x >= w - band || y < band || y >= h - band;\n                    if (!onBorder) continue;\n                    const i = (y * w + x) * 4;\n                    br += d[i];\n                    bg += d[i + 1];\n                    bb += d[i + 2];\n                    n++;\n                }\n            }\n            if (!n) return null;\n            br \/= n; bg \/= n; bb \/= n;\n\n            const borderDists = [];\n            const borderStep = 2;\n            for (let y = 0; y < h; y += borderStep) {\n                for (let x = 0; x < w; x += borderStep) {\n                    const onBorder = x < band || x >= w - band || y < band || y >= h - band;\n                    if (!onBorder) continue;\n                    const i = (y * w + x) * 4;\n                    borderDists.push(rgbDist(d[i], d[i + 1], d[i + 2], br, bg, bb));\n                }\n            }\n            if (!borderDists.length) return null;\n            borderDists.sort((a, b) => a - b);\n            const p95 = borderDists[Math.floor(borderDists.length * 0.95)];\n            const mean = borderDists.reduce((sum, v) => sum + v, 0) \/ borderDists.length;\n            const variance = borderDists.reduce((sum, v) => sum + (v - mean) * (v - mean), 0) \/ borderDists.length;\n            const std = Math.sqrt(variance);\n            const threshold = clamp(Math.max(35, p95 + std * 1.2), 35, 120);\n\n            const step = 2;\n            const mw = Math.max(1, Math.floor(w \/ step));\n            const mh = Math.max(1, Math.floor(h \/ step));\n            const mask = new Uint8Array(mw * mh);\n            for (let my = 0; my < mh; my++) {\n                const y = my * step;\n                for (let mx = 0; mx < mw; mx++) {\n                    const x = mx * step;\n                    const i = (y * w + x) * 4;\n                    const dist = rgbDist(d[i], d[i + 1], d[i + 2], br, bg, bb);\n                    if (dist >= threshold) mask[my * mw + mx] = 1;\n                }\n            }\n\n            const cleaned = new Uint8Array(mask);\n            for (let my = 1; my < mh - 1; my++) {\n                for (let mx = 1; mx < mw - 1; mx++) {\n                    const idx = my * mw + mx;\n                    if (!mask[idx]) continue;\n                    let neighbors = 0;\n                    for (let yy = -1; yy <= 1; yy++) {\n                        for (let xx = -1; xx <= 1; xx++) {\n                            if (!xx && !yy) continue;\n                            const ni = (my + yy) * mw + (mx + xx);\n                            if (mask[ni]) neighbors++;\n                        }\n                    }\n                    if (neighbors < 2) cleaned[idx] = 0;\n                }\n            }\n\n            const visited = new Uint8Array(mw * mh);\n            const queue = new Int32Array(mw * mh);\n            let best = null;\n            const targetRatios = [1050 \/ 600, 600 \/ 1050];\n\n            for (let start = 0; start < cleaned.length; start++) {\n                if (!cleaned[start] || visited[start]) continue;\n                let head = 0, tail = 0;\n                queue[tail++] = start;\n                visited[start] = 1;\n\n                let area = 0;\n                let minX = mw, minY = mh, maxX = -1, maxY = -1;\n                while (head < tail) {\n                    const idx = queue[head++];\n                    const y = Math.floor(idx \/ mw);\n                    const x = idx - y * mw;\n                    area++;\n                    if (x < minX) minX = x;\n                    if (y < minY) minY = y;\n                    if (x > maxX) maxX = x;\n                    if (y > maxY) maxY = y;\n                    if (x > 0) {\n                        const ni = idx - 1;\n                        if (cleaned[ni] && !visited[ni]) { visited[ni] = 1; queue[tail++] = ni; }\n                    }\n                    if (x + 1 < mw) {\n                        const ni = idx + 1;\n                        if (cleaned[ni] && !visited[ni]) { visited[ni] = 1; queue[tail++] = ni; }\n                    }\n                    if (y > 0) {\n                        const ni = idx - mw;\n                        if (cleaned[ni] && !visited[ni]) { visited[ni] = 1; queue[tail++] = ni; }\n                    }\n                    if (y + 1 < mh) {\n                        const ni = idx + mw;\n                        if (cleaned[ni] && !visited[ni]) { visited[ni] = 1; queue[tail++] = ni; }\n                    }\n                }\n\n                if (area < 120) continue;\n                const bw = maxX - minX + 1;\n                const bh = maxY - minY + 1;\n                if (bw <= 0 || bh <= 0) continue;\n                const boxArea = bw * bh;\n                const fill = area \/ Math.max(1, boxArea);\n                const coverage = boxArea \/ Math.max(1, mw * mh);\n                const ratio = bw \/ Math.max(1, bh);\n                const ratioDelta = Math.min(\n                    Math.abs(ratio - targetRatios[0]),\n                    Math.abs(ratio - targetRatios[1])\n                );\n                if (coverage < 0.04 || coverage > 0.9) continue;\n                if (fill < 0.35) continue;\n                if (ratio < 0.4 || ratio > 2.6) continue;\n\n                const score = coverage * 1.5 + fill * 1.2 + Math.max(0, 1 - ratioDelta \/ 1.2) * 1.3;\n                if (!best || score > best.score) best = { minX, minY, maxX, maxY, score };\n            }\n            if (!best) return null;\n\n            let x1 = best.minX * step;\n            let y1 = best.minY * step;\n            let x2 = Math.min(w - 1, best.maxX * step + (step - 1));\n            let y2 = Math.min(h - 1, best.maxY * step + (step - 1));\n            const bw = x2 - x1 + 1;\n            const bh = y2 - y1 + 1;\n            const padX = Math.round(bw * 0.08);\n            const padY = Math.round(bh * 0.08);\n            x1 = clamp(x1 - padX, 0, w - 1);\n            y1 = clamp(y1 - padY, 0, h - 1);\n            x2 = clamp(x2 + padX, 0, w - 1);\n            y2 = clamp(y2 + padY, 0, h - 1);\n\n            return { x: x1, y: y1, w: x2 - x1 + 1, h: y2 - y1 + 1 };\n        }\n\n        async function basicAutoCropFile(file) {\n            const img = await loadImageFromFile(file);\n            const srcW = img.naturalWidth || img.width;\n            const srcH = img.naturalHeight || img.height;\n            if (!srcW || !srcH) return file;\n\n            const maxSide = 1200;\n            const scale = Math.min(1, maxSide \/ Math.max(srcW, srcH));\n            const w = Math.max(1, Math.round(srcW * scale));\n            const h = Math.max(1, Math.round(srcH * scale));\n\n            const sampleCanvas = document.createElement('canvas');\n            sampleCanvas.width = w;\n            sampleCanvas.height = h;\n            const sctx = sampleCanvas.getContext('2d', { willReadFrequently: true });\n            sctx.drawImage(img, 0, 0, w, h);\n\n            const box = detectCardBoundsBasic(sampleCanvas, sctx);\n            if (!box) return file;\n\n            const sx = Math.round(box.x \/ scale);\n            const sy = Math.round(box.y \/ scale);\n            const sw = Math.round(box.w \/ scale);\n            const sh = Math.round(box.h \/ scale);\n            if (sw < 120 || sh < 80) return file;\n\n            const outCanvas = document.createElement('canvas');\n            outCanvas.width = sw;\n            outCanvas.height = sh;\n            const octx = outCanvas.getContext('2d');\n            octx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh);\n\n            return new Promise((resolve) => {\n                outCanvas.toBlob((blob) => {\n                    if (!blob) { resolve(file); return; }\n                    resolve(new File([blob], file.name.replace(\/\\.[^.]+$\/, '_basiccrop.jpg'), { type: 'image\/jpeg' }));\n                }, 'image\/jpeg', 0.95);\n            });\n        }\n\n        function trimCanvasByBasicBounds(outCanvas, extraPadRatio = 0.04) {\n            try {\n                const ctx = outCanvas.getContext('2d', { willReadFrequently: true });\n                if (!ctx) return false;\n                const box = detectCardBoundsBasic(outCanvas, ctx);\n                if (!box) return false;\n\n                const cw = outCanvas.width;\n                const ch = outCanvas.height;\n                if (!cw || !ch) return false;\n\n                const areaRatio = (box.w * box.h) \/ Math.max(1, cw * ch);\n                if (areaRatio < 0.08 || areaRatio > 0.98) return false;\n\n                const padX = Math.round(box.w * extraPadRatio);\n                const padY = Math.round(box.h * extraPadRatio);\n                const sx = clamp(box.x - padX, 0, cw - 1);\n                const sy = clamp(box.y - padY, 0, ch - 1);\n                const ex = clamp(box.x + box.w + padX, 1, cw);\n                const ey = clamp(box.y + box.h + padY, 1, ch);\n                const sw = Math.max(1, ex - sx);\n                const sh = Math.max(1, ey - sy);\n\n                const tmp = document.createElement('canvas');\n                tmp.width = sw;\n                tmp.height = sh;\n                const tctx = tmp.getContext('2d');\n                tctx.drawImage(outCanvas, sx, sy, sw, sh, 0, 0, sw, sh);\n\n                outCanvas.width = sw;\n                outCanvas.height = sh;\n                const octx = outCanvas.getContext('2d');\n                octx.drawImage(tmp, 0, 0, sw, sh);\n                return true;\n            } catch (_) {\n                return false;\n            }\n        }\n\n        async function maybeAutoCropUploadFile(file) {\n            \/\/ Gallery replacement\/upload should auto-crop first. If auto-crop cannot find\n            \/\/ a stable card boundary, keep the original and mark it for manual review.\n            try {\n                const cropped = await basicAutoCropFile(file);\n                if (cropped && cropped !== file && \/_basiccrop\\.\/i.test(cropped.name || '')) {\n                    cropped.__cittCropMode = 'basic';\n                    return cropped;\n                }\n            } catch (err) {\n                console.warn('[CITT] basic auto-crop failed, using review original', err);\n            }\n            try {\n                const rawName = file.name || \"namecard.jpg\";\n                const dot = rawName.lastIndexOf(\".\");\n                const ext = dot >= 0 ? rawName.slice(dot) : \".jpg\";\n                const base = dot >= 0 ? rawName.slice(0, dot) : rawName;\n                const reviewName = base + \"_review\" + ext;\n                const reviewFile = new File([file], reviewName, { type: file.type || \"image\/jpeg\" });\n                reviewFile.__cittCropMode = \"review\";\n                return reviewFile;\n            } catch (_) {\n                file.__cittCropMode = \"review\";\n                return file;\n            }\n        }\n\n        \/\/ Constants - Landscape (ngang)\n        const CARD_W_LANDSCAPE = 1050,\n            CARD_H_LANDSCAPE = 600,\n            TARGET_RATIO_LANDSCAPE = CARD_W_LANDSCAPE \/ CARD_H_LANDSCAPE;\n\n        \/\/ Constants - Portrait (d\u00e1\u00bb\u008dc)\n        const CARD_W_PORTRAIT = 600,\n            CARD_H_PORTRAIT = 1050,\n            TARGET_RATIO_PORTRAIT = CARD_W_PORTRAIT \/ CARD_H_PORTRAIT;\n\n        \/\/ Backward compatibility\n        const CARD_W = CARD_W_LANDSCAPE,\n            CARD_H = CARD_H_LANDSCAPE,\n            TARGET_RATIO = TARGET_RATIO_LANDSCAPE;\n\n        async function handleFiles(fileList) {\n            \/\/ Picker uploads use conservative canvas bounds cropping; do not wait for OpenCV.\n            const files = Array.from(fileList || []);\n            const promises = [];\n            for (const file of files) {\n                const mime = (file && file.type ? String(file.type) : '').toLowerCase();\n                const filename = (file && file.name ? String(file.name) : '');\n                const looksLikeImage = mime.startsWith('image\/') || \/\\.(jpe?g|png|gif|webp|bmp|heic|heif|tiff?)$\/i.test(filename);\n                if (!looksLikeImage) continue;\n                try {\n                    const sourceFile = await maybeAutoCropUploadFile(file);\n                    const img = await loadImage(sourceFile);\n                    const url = URL.createObjectURL(sourceFile);\n                    const originalUrl = URL.createObjectURL(file);\n                    promises.push(createItem(sourceFile.name || filename || 'image', img, url, file, originalUrl));\n                } catch (err) {\n                    console.warn('[CITT] skipped unreadable image from picker:', filename || '(no-name)', err);\n                }\n            }\n            if (!promises.length) console.warn('[CITT] no valid image files accepted from picker');\n            return Promise.all(promises);\n        }\n\n\n\n        function loadImage(fileOrUrl) {\n            return new Promise((res, rej) => {\n                if (fileOrUrl instanceof File) {\n                    const reader = new FileReader();\n                    reader.onload = (ev) => {\n                        const arrayBuffer = ev.target.result;\n                        let orientation = 1;\n                        try {\n                            const tags = EXIF.readFromBinaryFile(arrayBuffer);\n                            if (tags && tags.Orientation) orientation = tags.Orientation;\n                        } catch {}\n                        const sourceType = (fileOrUrl && fileOrUrl.type) ? fileOrUrl.type : 'application\/octet-stream';\n                        const blob = new Blob([arrayBuffer], { type: sourceType });\n                        const url = URL.createObjectURL(blob);\n                        const img = new Image();\n                        img.onload = () => {\n                            if (orientation !== 1) {\n                                const c = document.createElement('canvas');\n                                let cw = img.naturalWidth,\n                                    ch = img.naturalHeight;\n                                if (orientation >= 5 && orientation <= 8) {\n                                    c.width = ch;\n                                    c.height = cw;\n                                } else {\n                                    c.width = cw;\n                                    c.height = ch;\n                                }\n                                const ctx = c.getContext('2d');\n                                switch (orientation) {\n                                    case 2:\n                                        ctx.transform(-1, 0, 0, 1, c.width, 0);\n                                        break;\n                                    case 3:\n                                        ctx.transform(-1, 0, 0, -1, c.width, c.height);\n                                        break;\n                                    case 4:\n                                        ctx.transform(1, 0, 0, -1, 0, c.height);\n                                        break;\n                                    case 5:\n                                        ctx.transform(0, 1, 1, 0, 0, 0);\n                                        break;\n                                    case 6:\n                                        ctx.transform(0, 1, -1, 0, c.height, 0);\n                                        break;\n                                    case 7:\n                                        ctx.transform(0, -1, -1, 0, c.height, c.width);\n                                        break;\n                                    case 8:\n                                        ctx.transform(0, -1, 1, 0, 0, c.width);\n                                        break;\n                                }\n                                ctx.drawImage(img, 0, 0);\n                                const out = new Image();\n                                out.onload = () => {\n                                    URL.revokeObjectURL(url);\n                                    res(out);\n                                };\n                                out.onerror = rej;\n                                out.src = c.toDataURL('image\/png');\n                            } else {\n                                URL.revokeObjectURL(url);\n                                res(img);\n                            }\n                        };\n                        img.onerror = () => {\n                            URL.revokeObjectURL(url);\n                            const rawUrl = URL.createObjectURL(fileOrUrl);\n                            const rawImg = new Image();\n                            rawImg.onload = () => {\n                                URL.revokeObjectURL(rawUrl);\n                                res(rawImg);\n                            };\n                            rawImg.onerror = (err) => {\n                                URL.revokeObjectURL(rawUrl);\n                                rej(err);\n                            };\n                            rawImg.src = rawUrl;\n                        };\n                        img.src = url;\n                    };\n                    reader.onerror = rej;\n                    reader.readAsArrayBuffer(fileOrUrl);\n                    return;\n                }\n                const i = new Image();\n                i.onload = () => res(i);\n                i.onerror = rej;\n                i.crossOrigin = 'anonymous';\n                i.src = fileOrUrl;\n            });\n        }\n\n        function createItem(name, imgEl, blobUrl, originalBlob, originalBlobUrl) {\n            const item = document.createElement('div');\n            item.className = 'item';\n            item.dataset.blobUrl = blobUrl;\n            if (originalBlobUrl) item.dataset.originalBlobUrl = originalBlobUrl;\n            item.__cittOriginalBlob = originalBlob || null;\n            item.dataset.pid = `${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n            item.innerHTML = `\n\n<div class=\"panes\" style=\"position: relative;\">\n  <button class=\"btnDelete danger\" title=\"X\u00c3\u00b3a \u00e1\u00ba\u00a3nh n\u00c3\u00a0y\" >\n    <img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/cancel.png\" alt=\"X\u00c3\u00b3a\" style=\"width: 16px; height: 16px;\">\n  <\/button>\n  <div class=\"pane\"><h3>\u00e1\u00ba\u00a2nh g\u00e1\u00bb\u2018c<\/h3><img class=\"orig\" alt=\"original\"\/><\/div>\n  <div class=\"pane\"><h3>\u00c4\u0090\u00c3\u00a3 x\u00e1\u00bb\u00ad l\u00c3\u00bd<\/h3><div class=\"overlay\"><canvas class=\"out\" width=\"${CARD_W_LANDSCAPE}\" height=\"${CARD_H_LANDSCAPE}\"><\/canvas><div class=\"help\" style=\"display:none\">K\u00c3\u00a9o 4 \u00c4\u2018i\u00e1\u00bb\u0192m \u00c4\u2018\u00e1\u00bb\u0192 ch\u00e1\u00bb\u2030nh tay. Nh\u00e1\u00ba\u00a5n <span class=\"kbd\">Enter<\/span> \u00c4\u2018\u00e1\u00bb\u0192 x\u00c3\u00a1c nh\u00e1\u00ba\u00adn.<\/div><\/div><\/div>\n<\/div>\n\n \n<\/div>`;\n            grid.prepend(item);\n            item.querySelector('img.orig').src = originalBlobUrl || blobUrl;\n            const canvas = item.querySelector('canvas.out');\n            const ctx = canvas.getContext('2d');\n\n            const normalizedName = String(name || '');\n            const isCameraShot = \/^camera_\/.test(normalizedName);\n            const lowerName = normalizedName.toLowerCase();\n            const isPreCropped = \/_(fixed|basiccrop|review)\\.(png|jpe?g|webp|bmp)\/i.test(lowerName);\n            item.dataset.cropMode = \/_review\\.\/i.test(lowerName) ? \"review\" : (\/_basiccrop\\.\/i.test(lowerName) ? \"basic\" : \"\");\n            if (isCameraShot || isPreCropped) {\n                try {\n                    const w = imgEl.naturalWidth || imgEl.width;\n                    const h = imgEl.naturalHeight || imgEl.height;\n                    if (w && h) {\n                        canvas.width = w;\n                        canvas.height = h;\n                        ctx.imageSmoothingEnabled = true;\n                        ctx.imageSmoothingQuality = 'high';\n                        ctx.drawImage(imgEl, 0, 0, w, h);\n                        if (!isPreCropped) trimCanvasByBasicBounds(canvas, 0.06);\n                    } else {\n                        ctx.fillStyle = '#0b0c0f';\n                        ctx.fillRect(0, 0, canvas.width, canvas.height);\n                    }\n                } catch (e) {\n                    console.warn('[CITT] camera direct render failed', e);\n                    ctx.fillStyle = '#0b0c0f';\n                    ctx.fillRect(0, 0, canvas.width, canvas.height);\n                }\n                updateBulkState();\n                return Promise.resolve();\n            }\n            ctx.fillStyle = '#0b0c0f';\n            ctx.fillRect(0, 0, canvas.width, canvas.height);\n            const fallbackRender = () => {\n                try {\n                    const w = imgEl.naturalWidth || imgEl.width;\n                    const h = imgEl.naturalHeight || imgEl.height;\n                    if (w && h) {\n                        canvas.width = w;\n                        canvas.height = h;\n                        ctx.imageSmoothingEnabled = true;\n                        ctx.imageSmoothingQuality = 'high';\n                        ctx.drawImage(imgEl, 0, 0, w, h);\n                    }\n                } catch (e) {\n                    console.warn('[CITT] image render failed', e);\n                }\n            };\n\n            \/\/ Prefer basic crop first (stable, less distortion) before AI perspective warp.\n            fallbackRender();\n            const basicDone = trimCanvasByBasicBounds(canvas, 0.06);\n            if (basicDone) {\n                updateBulkState();\n                return Promise.resolve();\n            }\n\n            if (!cvReady || typeof cv === 'undefined') {\n                updateBulkState();\n                return Promise.resolve();\n            }\n\n            return autoProcess(imgEl, canvas, item)\n                .then(() => {\n                    trimCanvasByBasicBounds(canvas, 0.06);\n                    updateBulkState();\n                })\n                .catch((e) => {\n                    console.warn('[CITT] autoProcess failed, fallback original render', e);\n                    fallbackRender();\n                    trimCanvasByBasicBounds(canvas, 0.06);\n                    updateBulkState();\n                });\n\n\n            \/\/ item.querySelector('.btnDownload').onclick = async () => {\n            \/\/     canvas.toBlob(async (b) => {\n            \/\/         await smartSaveBlob(b, name.replace(\/\\.[^.]+$\/, '') + \"_fixed.png\")\n            \/\/     }, 'image\/png');\n            \/\/ };\n\n\n            const btnDel = item.querySelector('.btnDelete');\n            if (btnDel) {\n                btnDel.onclick = () => {\n                    const urls = [item.dataset.blobUrl, item.dataset.originalBlobUrl].filter(Boolean);\n                    urls.forEach((u) => {\n                        try {\n                            URL.revokeObjectURL(u)\n                        } catch {}\n                    });\n                    item.querySelectorAll('.marker').forEach(m => m.remove());\n                    const help = item.querySelector('.overlay .help');\n                    if (help) help.style.display = 'none';\n                    item.remove();\n                    updateBulkState();\n                }\n            }\n\n            updateBulkState();\n        }\n\n        function toastErr(err, item) {\n            console.error(err);\n            const h = document.createElement('div');\n            h.className = 'hint';\n            h.style.color = '#ff8585';\n            h.style.padding = '0 12px 12px';\n            h.textContent = 'Kh\u00c3\u00b4ng nh\u00e1\u00ba\u00adn di\u00e1\u00bb\u2021n \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c namecard chu\u00e1\u00ba\u00a9n. H\u00c3\u00a3y d\u00c3\u00b9ng \u00e2\u20ac\u0153Ch\u00e1\u00bb\u2030nh tay g\u00c3\u00b3c\u00e2\u20ac\u009d.';\n            item.appendChild(h);\n        }\n\n        function rotateOut(canvas) {\n            const src = document.createElement('canvas');\n            src.width = canvas.width;\n            src.height = canvas.height;\n            src.getContext('2d').drawImage(canvas, 0, 0);\n            const tmp = document.createElement('canvas');\n            tmp.width = src.height;\n            tmp.height = src.width;\n            const tctx = tmp.getContext('2d');\n            tctx.translate(tmp.width \/ 2, tmp.height \/ 2);\n            tctx.rotate(Math.PI \/ 2);\n            tctx.drawImage(src, -src.width \/ 2, -src.height \/ 2);\n\n            \/\/ Sau khi rotate, k\u00c3\u00adch th\u00c6\u00b0\u00e1\u00bb\u203ac \u00c4\u2018\u00e1\u00bb\u2022i: ngang -> d\u00e1\u00bb\u008dc ho\u00e1\u00ba\u00b7c d\u00e1\u00bb\u008dc -> ngang\n            const newW = tmp.width;\n            const newH = tmp.height;\n            canvas.width = newW;\n            canvas.height = newH;\n            const ctx = canvas.getContext('2d');\n            ctx.drawImage(tmp, 0, 0, newW, newH);\n        }\n\n\n\n        function orderPoints(pts) {\n            const sum = pts.map(p => p.x + p.y);\n            const diff = pts.map(p => p.y - p.x);\n            const tl = pts[sum.indexOf(Math.min(...sum))];\n            const br = pts[sum.indexOf(Math.max(...sum))];\n            const tr = pts[diff.indexOf(Math.min(...diff))];\n            const bl = pts[diff.indexOf(Math.max(...diff))];\n            return [tl, tr, bl, br];\n        }\n\n        \/\/ M\u00e1\u00bb\u0178 r\u00e1\u00bb\u2122ng 4 \u00c4\u2018i\u00e1\u00bb\u0192m ra ngo\u00c3\u00a0i ~5% t\u00e1\u00bb\u00ab t\u00c3\u00a2m \u00c4\u2018\u00e1\u00bb\u0192 crop kh\u00c3\u00b4ng qu\u00c3\u00a1 s\u00c3\u00a1t\n        function expandPoints(pts, imgW, imgH, expandRatio) {\n            expandRatio = expandRatio || 0.10;\n            const cx = pts.reduce((s, p) => s + p.x, 0) \/ pts.length;\n            const cy = pts.reduce((s, p) => s + p.y, 0) \/ pts.length;\n            return pts.map(p => ({\n                x: Math.max(0, Math.min(imgW, p.x + (p.x - cx) * expandRatio)),\n                y: Math.max(0, Math.min(imgH, p.y + (p.y - cy) * expandRatio))\n            }));\n        }\n\n        function quadAreaApprox(pts) {\n            const xs = pts.map(p => p.x),\n                ys = pts.map(p => p.y);\n            const w = Math.max(...xs) - Math.min(...xs);\n            const h = Math.max(...ys) - Math.min(...ys);\n            return w * h;\n        }\n\n        function angleCos(p0, p1, p2) {\n            const dx1 = p0.x - p1.x,\n                dy1 = p0.y - p1.y;\n            const dx2 = p2.x - p1.x,\n                dy2 = p2.y - p1.y;\n            const dot = dx1 * dx2 + dy1 * dy2;\n            const m1 = Math.hypot(dx1, dy1),\n                m2 = Math.hypot(dx2, dy2);\n            return Math.abs(dot \/ (m1 * m2 + 1e-6));\n        }\n\n        function rectangularityScore(pts) {\n            const [a, b, c, d] = orderPoints(pts);\n            const cos1 = angleCos(a, b, c);\n            const cos2 = angleCos(b, c, d);\n            const cos3 = angleCos(c, d, a);\n            const cos4 = angleCos(d, a, b);\n            const avg = (cos1 + cos2 + cos3 + cos4) \/ 4;\n            return 1 - Math.min(avg, 1);\n        }\n\n\n        function ratioFromPts(pts) {\n            \/\/ T\u00c3\u00adnh ratio t\u00e1\u00bb\u00ab c\u00e1\u00ba\u00a1nh th\u00e1\u00bb\u00b1c c\u00e1\u00bb\u00a7a quad (ordered: tl, tr, bl, br)\n            const ordered = orderPoints(pts);\n            const [tl, tr, bl, br] = ordered;\n            const topW = Math.hypot(tr.x - tl.x, tr.y - tl.y);\n            const botW = Math.hypot(br.x - bl.x, br.y - bl.y);\n            const leftH = Math.hypot(bl.x - tl.x, bl.y - tl.y);\n            const rightH = Math.hypot(br.x - tr.x, br.y - tr.y);\n            const avgW = (topW + botW) \/ 2;\n            const avgH = (leftH + rightH) \/ 2;\n            return avgW \/ (avgH || 1);\n        }\n\n        function borderTouchCount(pts, imgW, imgH, margin) {\n            let count = 0;\n            const xs = pts.map(p => p.x);\n            const ys = pts.map(p => p.y);\n            const minX = Math.min(...xs);\n            const minY = Math.min(...ys);\n            const maxX = Math.max(...xs);\n            const maxY = Math.max(...ys);\n            if (minX <= margin) count++;\n            if (minY <= margin) count++;\n            if (maxX >= imgW - margin) count++;\n            if (maxY >= imgH - margin) count++;\n            return count;\n        }\n\n        function scoreQuad(pts, imgArea) {\n            const area = quadAreaApprox(pts);\n            const coverage = Math.max(0, Math.min(1, area \/ imgArea));\n            const r = ratioFromPts(pts);\n\n            \/\/ X\u00c3\u00a1c \u00c4\u2018\u00e1\u00bb\u2039nh orientation: n\u00e1\u00ba\u00bfu r > 1 th\u00c3\u00ac ngang, ng\u00c6\u00b0\u00e1\u00bb\u00a3c l\u00e1\u00ba\u00a1i th\u00c3\u00ac d\u00e1\u00bb\u008dc\n            const isLandscape = r > 1;\n            const targetRatio = isLandscape ? TARGET_RATIO_LANDSCAPE : TARGET_RATIO_PORTRAIT;\n\n            \/\/ Tighter ratio scoring and basic rejection for wildly different aspect ratios\n            const ratioDiff = Math.abs(r - targetRatio);\n            if (ratioDiff > 0.6) {\n                return 0; \/\/ aspect too far from expected -> reject\n            }\n\n            const ratioScore = 1 - Math.min(1, ratioDiff \/ 0.5);\n            const rectScore = rectangularityScore(pts);\n\n            \/\/ Penalize near-full-frame quads (often table\/photo border, not card).\n            if (coverage > 0.92) {\n                return ratioScore * 0.2 + rectScore * 0.2;\n            }\n\n            \/\/ Require a minimum rectangularity and coverage to be considered reliable\n            if (coverage < 0.06) {\n                return 0; \/\/ Hard reject: quad too small, likely an internal element\n            }\n            if (rectScore < 0.45) {\n                \/\/ return diminished score (still allow fallback attempts)\n                return Math.max(0, ratioScore * 0.3 + rectScore * 0.2 + coverage * 0.05);\n            }\n\n            \/\/ Weighted final score (more emphasis on ratio + rectangularity)\n            return ratioScore * 0.5 + rectScore * 0.35 + coverage * 0.15;\n        }\n\n\n\n        function fourPointTransform(src, points, W = null, H = null) {\n            \/\/ N\u00e1\u00ba\u00bfu kh\u00c3\u00b4ng truy\u00e1\u00bb\u0081n W, H th\u00c3\u00ac t\u00e1\u00bb\u00b1 \u00c4\u2018\u00e1\u00bb\u2122ng x\u00c3\u00a1c \u00c4\u2018\u00e1\u00bb\u2039nh d\u00e1\u00bb\u00b1a tr\u00c3\u00aan ratio\n            if (W === null || H === null) {\n                const r = ratioFromPts(points);\n                const isLandscape = r > 1;\n                W = isLandscape ? CARD_W_LANDSCAPE : CARD_W_PORTRAIT;\n\n                H = isLandscape ? CARD_H_LANDSCAPE : CARD_H_PORTRAIT;\n\n            }\n\n            const dst = cv.matFromArray(4, 1, cv.CV_32FC2, new Float32Array([0, 0, W, 0, 0, H, W, H]));\n            const srcPts = cv.matFromArray(4, 1, cv.CV_32FC2, new Float32Array([points[0].x, points[0].y, points[1].x,\n                points[1].y, points[2].x, points[2].y, points[3].x, points[3].y\n            ]));\n            const M = cv.getPerspectiveTransform(srcPts, dst);\n            const out = new cv.Mat();\n            const dsize = new cv.Size(W, H);\n            cv.warpPerspective(src, out, M, dsize, cv.INTER_CUBIC, cv.BORDER_REPLICATE);\n            srcPts.delete();\n            dst.delete();\n            M.delete();\n            return out;\n        }\n\n\n\n        function enhanceCard(src) {\n            let bgr = new cv.Mat();\n            cv.cvtColor(src, bgr, cv.COLOR_RGBA2BGR);\n            let lab = new cv.Mat();\n            cv.cvtColor(bgr, lab, cv.COLOR_BGR2Lab);\n            let planes = new cv.MatVector();\n            cv.split(lab, planes);\n            let clahe = new cv.CLAHE(2.0, new cv.Size(8, 8));\n            clahe.apply(planes.get(0), planes.get(0));\n            cv.merge(planes, lab);\n            let outBgr = new cv.Mat();\n            cv.cvtColor(lab, outBgr, cv.COLOR_Lab2BGR);\n            let blur = new cv.Mat();\n            cv.GaussianBlur(outBgr, blur, new cv.Size(0, 0), 1.0);\n            let sharp = new cv.Mat();\n            cv.addWeighted(outBgr, 1.35, blur, -0.35, 0, sharp);\n            let out = new cv.Mat();\n            cv.cvtColor(sharp, out, cv.COLOR_BGR2RGBA);\n            [bgr, lab, planes, outBgr, blur, sharp].forEach(m => {\n                if (m && !m.isDeleted()) m.delete()\n            });\n            clahe.delete();\n            return out;\n        }\n\n        function autoProcess(imgEl, outCanvas, item) {\n            return new Promise((resolve, reject) => {\n                let full = cv.imread(imgEl);\n\n                let work = new cv.Mat();\n                full.copyTo(work);\n                const maxSide = 1400;\n                let ratio = 1;\n                if (Math.max(work.rows, work.cols) > maxSide) {\n                    ratio = Math.max(work.rows, work.cols) \/ maxSide;\n                    let dsize = new cv.Size(Math.round(work.cols \/ ratio), Math.round(work.rows \/ ratio));\n                    let tmp = new cv.Mat();\n                    cv.resize(work, tmp, dsize, 0, 0, cv.INTER_AREA);\n                    work.delete();\n                    work = tmp;\n                    tmp = null;\n                }\n                \/\/ Determine input orientation from original image dimensions (width = image_width, height = image_height)\n                let origW = full.cols;\n                let origH = full.rows;\n                let rotatedInput = false; \/\/ true if we rotated input 90deg clockwise for processing\n                if (origH > origW) {\n                    cv.rotate(work, work, cv.ROTATE_90_CLOCKWISE);\n                    cv.rotate(full, full, cv.ROTATE_90_CLOCKWISE);\n                    rotatedInput = true;\n                }\n\n                let gray = new cv.Mat();\n                cv.cvtColor(work, gray, cv.COLOR_RGBA2GRAY);\n                let blur = new cv.Mat();\n                cv.GaussianBlur(gray, blur, new cv.Size(5, 5), 0);\n                let th = new cv.Mat();\n                cv.adaptiveThreshold(blur, th, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2);\n\n                \/\/ Ph\u00c6\u00b0\u00c6\u00a1ng ph\u00c3\u00a1p edge detection c\u00e1\u00ba\u00a3i ti\u00e1\u00ba\u00bfn\n                let edges1 = new cv.Mat();\n                cv.Canny(blur, edges1, 30, 100); \/\/ Threshold th\u00e1\u00ba\u00a5p h\u00c6\u00a1n\n                let edges2 = new cv.Mat();\n                cv.Canny(blur, edges2, 50, 150); \/\/ Threshold trung b\u00c3\u00acnh\n                let edged = new cv.Mat();\n                cv.bitwise_or(edges1, edges2, edged);\n                edges1.delete();\n                edges2.delete();\n\n                \/\/ Morphology operations\n                let kernel = cv.Mat.ones(3, 3, cv.CV_8U);\n                let dilated = new cv.Mat();\n                cv.dilate(edged, dilated, kernel);\n                let closed = new cv.Mat();\n                cv.morphologyEx(dilated, closed, cv.MORPH_CLOSE, kernel);\n                dilated.delete();\n                let contours = new cv.MatVector();\n                let hierarchy = new cv.Mat();\n                cv.findContours(closed, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);\n                const imgArea = work.rows * work.cols;\n                let bestPts = null;\n                let bestScore = 0;\n                for (let i = 0; i < contours.size(); i++) {\n                    const cnt = contours.get(i);\n                    const peri = cv.arcLength(cnt, true);\n                    const approx = new cv.Mat();\n                    cv.approxPolyDP(cnt, approx, 0.02 * peri, true);\n                    let pts = null;\n                    if (approx.rows === 4) {\n                        pts = [];\n                        for (let r = 0; r < 4; r++) pts.push({\n                            x: approx.intAt(r, 0) * ratio,\n                            y: approx.intAt(r, 1) * ratio\n                        });\n                    } else {\n                        const rect = cv.minAreaRect(cnt);\n                        const box = cv.RotatedRect.points(rect);\n                        if (box) {\n                            pts = [];\n                            for (let k = 0; k < 4; k++) pts.push({\n                                x: box[k].x * ratio,\n                                y: box[k].y * ratio\n                            });\n                        }\n                    }\n                    if (pts) {\n                        const sc = scoreQuad(pts, imgArea * ratio * ratio);\n                        if (sc > bestScore) {\n                            bestScore = sc;\n                            bestPts = pts;\n                        }\n                    }\n                    approx.delete();\n                    cnt.delete();\n                }\n                const SCORE_THRESHOLD_UPLOAD = 0.38;\n                if (!bestPts || bestScore < SCORE_THRESHOLD_UPLOAD) {\n\n                    \/\/ Try alternate-orientation detection when primary score is low.\n                    \/\/ If input was rotated at the beginning, fallback rotates back the opposite way.\n                    if (true) {\n                        let rotatedWork = new cv.Mat();\n                        let rotatedFull = new cv.Mat();\n                        const rotateDetectCode = rotatedInput ? cv.ROTATE_90_COUNTERCLOCKWISE : cv.ROTATE_90_CLOCKWISE;\n                        const rotateBackCode = rotatedInput ? cv.ROTATE_90_CLOCKWISE : cv.ROTATE_90_COUNTERCLOCKWISE;\n                        cv.rotate(work, rotatedWork, rotateDetectCode);\n                        cv.rotate(full, rotatedFull, rotateDetectCode);\n\n                        \/\/ Re-run contour-finding on rotatedWork\n                        let contours2 = new cv.MatVector();\n                        let hierarchy2 = new cv.Mat();\n                        let edgesR = new cv.Mat();\n                        cv.Canny(rotatedWork, edgesR, 30, 150);\n                        let kernelR = cv.Mat.ones(3, 3, cv.CV_8U);\n                        let dilR = new cv.Mat();\n                        cv.dilate(edgesR, dilR, kernelR);\n                        let closedR = new cv.Mat();\n                        cv.morphologyEx(dilR, closedR, cv.MORPH_CLOSE, kernelR);\n                        cv.findContours(closedR, contours2, hierarchy2, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);\n\n                        let bestPtsR = null;\n                        let bestScoreR = 0;\n                        const imgAreaR = rotatedWork.rows * rotatedWork.cols;\n                        for (let i = 0; i < contours2.size(); i++) {\n                            const cnt = contours2.get(i);\n                            const peri = cv.arcLength(cnt, true);\n                            const approx = new cv.Mat();\n                            cv.approxPolyDP(cnt, approx, 0.02 * peri, true);\n                            let pts = null;\n                            if (approx.rows === 4) {\n                                pts = [];\n                                for (let r = 0; r < 4; r++) pts.push({\n                                    x: approx.intAt(r, 0),\n                                    y: approx.intAt(r, 1)\n                                });\n                            } else {\n                                const rect = cv.minAreaRect(cnt);\n                                const box = cv.RotatedRect.points(rect);\n                                if (box) {\n                                    pts = [];\n                                    for (let k = 0; k < 4; k++) pts.push({\n                                        x: box[k].x,\n                                        y: box[k].y\n                                    });\n                                }\n                            }\n                            if (pts) {\n                                const sc = scoreQuad(pts, imgAreaR);\n                                if (sc > bestScoreR) {\n                                    bestScoreR = sc;\n                                    bestPtsR = pts;\n                                }\n                            }\n                            approx.delete();\n                            cnt.delete();\n                        }\n\n                        if (bestPtsR && bestScoreR >= SCORE_THRESHOLD_UPLOAD) {\n                            const orderedR = orderPoints(bestPtsR);\n                const expandedR = expandPoints(orderedR, rotatedFull.cols, rotatedFull.rows, 0.12);\n                            const warpedR = fourPointTransform(rotatedFull,\n                                expandedR); \/\/ generates correct oriented card but rotated by 90\u00c2\u00b0 + padding 5%\n                            let enhancedR = enhanceCard(warpedR);\n\n                            \/\/ Rotate back to original orientation\n                            let corrected = new cv.Mat();\n                            cv.rotate(enhancedR, corrected, rotateBackCode);\n                            enhancedR.delete();\n\n                            \/\/ Normalize corrected if it looks like a landscape card but cols < rows (rotated by warp)\n                            let normalizedRfRot = false;\n                            try {\n                                const longShortR = Math.max(corrected.cols, corrected.rows) \/ Math.max(1, Math.min(\n                                    corrected.cols, corrected.rows));\n                                if (corrected.cols < corrected.rows && Math.abs(longShortR -\n                                        TARGET_RATIO_LANDSCAPE) < 0.4) {\n                                    const tmpR = new cv.Mat();\n                                    cv.rotate(corrected, tmpR, cv.ROTATE_90_CLOCKWISE);\n                                    if (corrected && !corrected.isDeleted()) corrected.delete();\n                                    corrected = tmpR;\n                                    normalizedRfRot = true;\n                                }\n                            } catch (e) {\n                                console.warn('Normalized fallback check failed:', e);\n                            }\n\n                            \/\/ Compute net rotation required so final orientation (relative to original image) is as requested\n                            \/\/ Determine desired rotation based on corrected (post-rotated-back) dimensions:\n                            const desiredDegRf = (corrected.cols >= corrected.rows) ? 0 : 90;\n                            const currentDegRf = normalizedRfRot ? 90 :\n                                0; \/\/ corrected is in original orientation, add 90 if we rotated it in normalization\n                            const needDegRf = (desiredDegRf - currentDegRf + 360) % 360;\n                            let final = new cv.Mat();\n                            if (needDegRf === 0) {\n                                corrected.copyTo(final);\n                            } else if (needDegRf === 90) {\n                                cv.rotate(corrected, final, cv.ROTATE_90_CLOCKWISE);\n                            } else if (needDegRf === 180) {\n                                cv.rotate(corrected, final, cv.ROTATE_180);\n                            } else if (needDegRf === 270) {\n                                cv.rotate(corrected, final, cv.ROTATE_90_COUNTERCLOCKWISE);\n                            } else {\n                                corrected.copyTo(final);\n                            }\n\n                            \/\/ Ensure final output uses target dimensions (landscape default) to avoid downstream mis-cropping\n                            const desiredW = (final.cols >= final.rows) ? CARD_W_LANDSCAPE : CARD_W_PORTRAIT;\n                            const desiredH = (final.cols >= final.rows) ? CARD_H_LANDSCAPE : CARD_H_PORTRAIT;\n                            if (final.cols !== desiredW || final.rows !== desiredH) {\n                                const resized = new cv.Mat();\n                                cv.resize(final, resized, new cv.Size(desiredW, desiredH), 0, 0, cv.INTER_CUBIC);\n                                if (final && !final.isDeleted()) final.delete();\n                                final = resized;\n                            }\n\n                            outCanvas.width = final.cols;\n                            outCanvas.height = final.rows;\n                            cv.imshow(outCanvas, final);\n\n                            \/\/ Cleanup rotated mats\n                            [rotatedWork, rotatedFull, contours2, hierarchy2, edgesR, kernelR, dilR, closedR,\n                                warpedR, enhancedR, corrected, final\n                            ].forEach(m => {\n                                if (m && !m.isDeleted()) m.delete();\n                            });\n\n                            [work, gray, blur, th, edged, kernel, closed, contours, hierarchy, full].forEach(m => {\n                                if (m && !m.isDeleted()) m.delete();\n                            });\n                            return resolve();\n                        }\n\n                        \/\/ N\u00e1\u00ba\u00bfu v\u00e1\u00ba\u00abn kh\u00c3\u00b4ng ph\u00c3\u00a1t hi\u00e1\u00bb\u2021n \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c trong rotated fallback, cleanup rotated mats and continue to fallback display below\n                        [rotatedWork, rotatedFull, contours2, hierarchy2, edgesR, kernelR, dilR, closedR].forEach(\n                            m => {\n                                if (m && !m.isDeleted()) m.delete();\n                            });\n                    }\n\n                    \/\/ N\u00e1\u00ba\u00bfu v\u00e1\u00ba\u00abn kh\u00c3\u00b4ng ph\u00c3\u00a1t hi\u00e1\u00bb\u2021n \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c, hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039 \u00e1\u00ba\u00a3nh g\u00e1\u00bb\u2018c nh\u00e1\u00bb\u008f g\u00e1\u00bb\u008dn l\u00c3\u00a0m fallback\n                    const ctx = outCanvas.getContext('2d');\n                    ctx.clearRect(0, 0, outCanvas.width, outCanvas.height);\n                    const tmpDraw = document.createElement('canvas');\n                    \/\/ Rotate fallback display according to original orientation (landscape = no rotation, portrait = +90\u00c2\u00b0)\n                    const showMat = new cv.Mat();\n                    if (origW >= origH) {\n                        \/\/ Landscape: no rotation (show as-is)\n                        full.copyTo(showMat);\n                    } else {\n                        \/\/ Portrait: rotate clockwise 90\u00c2\u00b0\n                        cv.rotate(full, showMat, cv.ROTATE_90_CLOCKWISE);\n                    }\n                    tmpDraw.width = showMat.cols;\n                    tmpDraw.height = showMat.rows;\n                    cv.imshow(tmpDraw, showMat);\n                    outCanvas.width = tmpDraw.width;\n                    outCanvas.height = tmpDraw.height;\n                    ctx.drawImage(tmpDraw, 0, 0, outCanvas.width, outCanvas.height);\n                    if (showMat && !showMat.isDeleted()) showMat.delete();\n\n                    [work, gray, blur, th, edged, kernel, closed, contours, hierarchy, full].forEach(m => {\n                        if (m && !m.isDeleted()) m.delete();\n                    });\n                    return reject(new Error('No reliable quad (score: ' + (bestScore || 0).toFixed(3) + ')'));\n                }\n                const ordered = orderPoints(bestPts);\n\n                \/\/ Coverage check: n\u00e1\u00ba\u00bfu quad chi\u00e1\u00ba\u00bfm >85% \u00e1\u00ba\u00a3nh \u00e2\u2020\u2019 \u00e1\u00ba\u00a3nh \u00c4\u2018\u00c3\u00a3 l\u00c3\u00a0 card g\u00e1\u00bb\u2018c \u00e2\u2020\u2019 skip warp\n                const fullArea = full.cols * full.rows;\n                const quadArea = quadAreaApprox(ordered);\n                const coverageRatio = quadArea \/ fullArea;\n                if (coverageRatio > 0.85) {\n                    const w = imgEl.naturalWidth || imgEl.width;\n                    const h = imgEl.naturalHeight || imgEl.height;\n                    if (w && h) {\n                        outCanvas.width = w;\n                        outCanvas.height = h;\n                        const ctx2 = outCanvas.getContext('2d');\n                        ctx2.imageSmoothingEnabled = true;\n                        ctx2.imageSmoothingQuality = 'high';\n                        ctx2.drawImage(imgEl, 0, 0, w, h);\n                    }\n                    [work, gray, blur, th, edged, kernel, closed, contours, hierarchy, full].forEach(m => {\n                        if (m && !m.isDeleted()) m.delete();\n                    });\n                    return resolve();\n                }\n\n                const expanded = expandPoints(ordered, full.cols, full.rows, 0.15);\n                const warped = fourPointTransform(full, expanded); \/\/ T\u00e1\u00bb\u00b1 \u00c4\u2018\u00e1\u00bb\u2122ng x\u00c3\u00a1c \u00c4\u2018\u00e1\u00bb\u2039nh k\u00c3\u00adch th\u00c6\u00b0\u00e1\u00bb\u203ac + padding 10%\n                let enhanced = enhanceCard(warped);\n\n                \/\/ Normalize orientation: if the enhanced image is taller than wide but its long\/short ratio matches the landscape target,\n                \/\/ rotate it 90\u00c2\u00b0 to make it landscape (this fixes cases where warping chose portrait sizing while the card is actually landscape).\n                let normalizedWasRotated = false;\n                try {\n                    const longShort = Math.max(enhanced.cols, enhanced.rows) \/ Math.max(1, Math.min(enhanced.cols,\n                        enhanced.rows));\n                    if (enhanced.cols < enhanced.rows && Math.abs(longShort - TARGET_RATIO_LANDSCAPE) < 0.4) {\n                        const tmp = new cv.Mat();\n                        cv.rotate(enhanced, tmp, cv.ROTATE_90_CLOCKWISE);\n                        enhanced.delete();\n                        enhanced = tmp;\n                        normalizedWasRotated = true;\n                    }\n                } catch (e) {\n                    console.warn('Normalization check failed:', e);\n                }\n\n                \/\/ Compute net rotation required so final orientation (relative to original image) is as requested:\n                \/\/ Determine desired rotation based on enhanced output dimensions (use enhanced cols\/rows).\n                \/\/ Landscape final: 0\u00c2\u00b0 (no rotation \/ 360\u00c2\u00b0). Portrait final: 90\u00c2\u00b0.\n                const desiredDeg = (enhanced.cols >= enhanced.rows) ? 0 : 90;\n                let currentDeg = rotatedInput ? 90 : 0; \/\/ current orientation of `enhanced` relative to original\n                if (normalizedWasRotated) currentDeg = (currentDeg + 90) %\n                    360; \/\/ account for our normalization rotation\n                const needDeg = (desiredDeg - currentDeg + 360) % 360;\n                let final = new cv.Mat();\n                if (needDeg === 0) {\n                    enhanced.copyTo(final);\n                } else if (needDeg === 90) {\n                    cv.rotate(enhanced, final, cv.ROTATE_90_CLOCKWISE);\n                } else if (needDeg === 180) {\n                    cv.rotate(enhanced, final, cv.ROTATE_180);\n                } else if (needDeg === 270) {\n                    cv.rotate(enhanced, final, cv.ROTATE_90_COUNTERCLOCKWISE);\n                } else {\n                    \/\/ Fallback copy if unexpected\n                    enhanced.copyTo(final);\n                }\n\n                \/\/ Ensure final output uses target dimensions (landscape default) to avoid downstream mis-cropping\n                const desiredW = (final.cols >= final.rows) ? CARD_W_LANDSCAPE : CARD_W_PORTRAIT;\n                const desiredH = (final.cols >= final.rows) ? CARD_H_LANDSCAPE : CARD_H_PORTRAIT;\n                if (final.cols !== desiredW || final.rows !== desiredH) {\n                    const resized = new cv.Mat();\n                    cv.resize(final, resized, new cv.Size(desiredW, desiredH), 0, 0, cv.INTER_CUBIC);\n                    if (final && !final.isDeleted()) final.delete();\n                    final = resized;\n                }\n\n                outCanvas.width = final.cols;\n                outCanvas.height = final.rows;\n                cv.imshow(outCanvas, final);\n\n                \/\/ Cleanup all Mats\n                [final, enhanced, warped, work, gray, blur, th, edged, kernel, closed, contours, hierarchy, full]\n                .forEach(\n                    m => {\n                        if (m && !m.isDeleted()) m.delete()\n                    });\n                resolve();\n            });\n        }\n\n        function manualAdjust(imgEl, outCanvas, item) {\n            const overlay = item.querySelector('.overlay');\n            const help = overlay.querySelector('.help');\n            help.style.display = 'block';\n            overlay.style.position = 'relative';\n            const pickImg = item.querySelector('.pane .orig');\n            let pts = [{\n                x: 20,\n                y: 20\n            }, {\n                x: pickImg.clientWidth - 20,\n                y: 20\n            }, {\n                x: 20,\n                y: pickImg.clientHeight - 20\n            }, {\n                x: pickImg.clientWidth - 20,\n                y: pickImg.clientHeight - 20\n            }];\n            const markers = pts.map((p, i) => {\n                const m = document.createElement('div');\n                m.className = 'marker';\n                m.style.left = p.x + 'px';\n                m.style.top = p.y + 'px';\n                m.dataset.idx = i;\n                overlay.appendChild(m);\n                return m;\n            });\n            let dragging = null;\n            markers.forEach(m => {\n                m.addEventListener('pointerdown', e => {\n                    dragging = m;\n                    m.setPointerCapture(e.pointerId)\n                });\n                m.addEventListener('pointermove', e => {\n                    if (!dragging) return;\n                    const r = overlay.getBoundingClientRect();\n                    const x = Math.max(0, Math.min(r.width, e.clientX - r.left));\n                    const y = Math.max(0, Math.min(r.height, e.clientY - r.top));\n                    dragging.style.left = x + 'px';\n                    dragging.style.top = y + 'px';\n                    pts[+dragging.dataset.idx] = {\n                        x,\n                        y\n                    };\n                });\n                m.addEventListener('pointerup', () => dragging = null);\n            });\n            const confirm = (ev) => {\n                if (ev.key !== 'Enter') return;\n                const ov = overlay.getBoundingClientRect();\n                const imgRect = pickImg.getBoundingClientRect();\n                const scaleX = imgEl.naturalWidth \/ imgRect.width;\n                const scaleY = imgEl.naturalHeight \/ imgRect.height;\n                const offsetX = imgRect.left - ov.left;\n                const offsetY = imgRect.top - ov.top;\n                const mapped = orderPoints([{\n                    x: (pts[0].x - offsetX) * scaleX,\n                    y: (pts[0].y - offsetY) * scaleY\n                }, {\n                    x: (pts[1].x - offsetX) * scaleX,\n                    y: (pts[1].y - offsetY) * scaleY\n                }, {\n                    x: (pts[2].x - offsetX) * scaleX,\n                    y: (pts[2].y - offsetY) * scaleY\n                }, {\n                    x: (pts[3].x - offsetX) * scaleX,\n                    y: (pts[3].y - offsetY) * scaleY\n                }, ]);\n                let full = cv.imread(imgEl);\n                let warped = fourPointTransform(full, mapped); \/\/ T\u00e1\u00bb\u00b1 \u00c4\u2018\u00e1\u00bb\u2122ng x\u00c3\u00a1c \u00c4\u2018\u00e1\u00bb\u2039nh k\u00c3\u00adch th\u00c6\u00b0\u00e1\u00bb\u203ac\n                let enhanced = enhanceCard(warped);\n\n                \/\/ C\u00e1\u00ba\u00adp nh\u00e1\u00ba\u00adt k\u00c3\u00adch th\u00c6\u00b0\u00e1\u00bb\u203ac canvas theo output\n                outCanvas.width = enhanced.cols;\n                outCanvas.height = enhanced.rows;\n                cv.imshow(outCanvas, enhanced);\n                [full, warped, enhanced].forEach(m => m.delete());\n                markers.forEach(m => m.remove());\n                help.style.display = 'none';\n                document.removeEventListener('keydown', confirm);\n                updateBulkState();\n            };\n            document.addEventListener('keydown', confirm);\n        }\n\n        \/\/ ====== B (kept) behaviors for dependent selects\/checkboxes =====\n        document.getElementById('country').addEventListener('change', function() {\n            document.getElementById('region-select').style.display = this.value === 'Vietnam' ? 'block' : 'none';\n        });\n        document.addEventListener('DOMContentLoaded', function() {\n            const industryCheckboxes = document.querySelectorAll('input[name=\"industry[]\"]');\n            \/\/ const subOptionsWrapper = document.getElementById('sub-options');\n            industryCheckboxes.forEach(function(checkbox) {\n                checkbox.addEventListener('change', function() {\n                    const showMedical = document.querySelector(\n                        'input[name=\"industry[]\"][value=\"Medical\"]').checked;\n                    const showFactory = document.querySelector(\n                        'input[name=\"industry[]\"][value=\"Factory\"]').checked;\n                    const showAutomobile = document.querySelector(\n                        'input[name=\"industry[]\"][value=\"Automobile\"]').checked;\n                    const showTechnology = document.querySelector(\n                        'input[name=\"industry[]\"][value=\"Technology\"]').checked;\n                    document.getElementById('medical-sub').style.display = showMedical ? 'block' :\n                        'none';\n                    document.getElementById('factory-sub').style.display = showFactory ? 'block' :\n                        'none';\n                    const autoSub = document.getElementById('automobile-sub');\n                    if (autoSub) autoSub.style.display = showAutomobile ? 'block' : 'none';\n                    const techSub = document.getElementById('technology-sub');\n                    if (techSub) techSub.style.display = showTechnology ? 'block' : 'none';\n                    subOptionsWrapper.style.display = (showMedical || showFactory ||\n                        showAutomobile || showTechnology) ? 'block' : 'none';\n                });\n            });\n\n            const supplierSelect = document.getElementById('supplier_type');\n            const supplierOptionsWrapper = document.getElementById('supplier-options');\n            const businessOptions = document.getElementById('business-options');\n            const serviceOptions = document.getElementById('service-options');\n            const industryWrapper = document.getElementById('industry-wrapper');\n\n\n            const businessCheckboxes = document.querySelectorAll('#business-options input[type=\"checkbox\"]');\n            businessCheckboxes.forEach(function(checkbox) {\n                checkbox.addEventListener('change', function() {\n                    let anyChecked = Array.from(businessCheckboxes).some(cb => cb.checked);\n                    if (supplierSelect.value === 'Business') {\n                        industryWrapper.style.display = anyChecked ? 'block' : 'none';\n                    }\n                });\n            });\n        });\n\n        \/\/ ====== Bridge: Build gallery-item from processedStore and allow assigning to front\/back ======\n        const pickGallery = document.getElementById('pick-gallery-item');\n\n        function scrollToRemainingNamecards() {\n            const gallery = document.getElementById('pick-gallery-item');\n            if (!gallery) return;\n            const nextCard = gallery.querySelector('.card');\n            if (!nextCard) return;\n            const target = document.querySelector('#stage-b .title-step') || gallery;\n            requestAnimationFrame(() => {\n                target.scrollIntoView({ behavior: 'smooth', block: 'start' });\n            });\n        }\n\n        async function cittOptimizeBlobForOcr(blob, filename) {\n            const maxSide = 900;\n            const quality = 0.65;\n            if (!blob || !\/^image\\\/\/i.test(blob.type || '')) return blob;\n\n            const url = URL.createObjectURL(blob);\n            try {\n                const img = new Image();\n                img.decoding = 'async';\n                await new Promise((resolve, reject) => {\n                    img.onload = resolve;\n                    img.onerror = reject;\n                    img.src = url;\n                });\n\n                const w = img.naturalWidth || img.width || 0;\n                const h = img.naturalHeight || img.height || 0;\n                if (!w || !h) return blob;\n\n                const scale = Math.min(1, maxSide \/ Math.max(w, h));\n                const out = document.createElement('canvas');\n                out.width = Math.max(1, Math.round(w * scale));\n                out.height = Math.max(1, Math.round(h * scale));\n                const ctx = out.getContext('2d', { alpha: false });\n                ctx.fillStyle = '#fff';\n                ctx.fillRect(0, 0, out.width, out.height);\n                ctx.imageSmoothingEnabled = true;\n                ctx.imageSmoothingQuality = 'high';\n                ctx.drawImage(img, 0, 0, out.width, out.height);\n\n                const optimized = await new Promise(resolve => out.toBlob(resolve, 'image\/jpeg', quality));\n                if (!optimized) return blob;\n                const base = String(filename || 'namecard').replace(\/\\.[^.]+$\/, '');\n                return new File([optimized], base + '.jpg', { type: 'image\/jpeg' });\n            } catch (err) {\n                console.warn('[CITT] OCR image optimize failed, using original blob', err);\n                return blob;\n            } finally {\n                URL.revokeObjectURL(url);\n            }\n        }\n\n        function fileFromBlob(blob, name) {\n            try {\n                return new File([blob], name, {\n                    type: blob.type || 'image\/png'\n                })\n            } catch {\n                return null\n            }\n        }\n\n        function replaceInputFile(inputEl, file) {\n            const dt = new DataTransfer();\n            dt.items.add(file);\n            const newInput = document.createElement('input');\n            newInput.type = 'file';\n            newInput.name = inputEl.name;\n            newInput.id = inputEl.id;\n            newInput.accept = inputEl.accept;\n            newInput.required = inputEl.required;\n            newInput.style.display = 'none';\n            newInput.files = dt.files;\n            inputEl.parentNode.insertBefore(newInput, inputEl);\n            inputEl.remove();\n            return newInput;\n        }\n\n        function setPreview(elId, blob) {\n            const url = URL.createObjectURL(blob);\n            const prev = document.getElementById(elId);\n            if (!prev) return;\n            const oldImg = prev.querySelector('img[data-preview-src]');\n            const oldUrl = oldImg ? oldImg.dataset.previewSrc : '';\n            if (oldUrl && oldUrl.indexOf('blob:') === 0) {\n                try {\n                    URL.revokeObjectURL(oldUrl);\n                } catch (_) {}\n            }\n            prev.innerHTML = `<img decoding=\"async\" src=\"${url}\" data-preview-src=\"${url}\" alt=\"${elId}\">`;\n        }\n\n        async function assignTo(which, id) { \/\/ which: 'before' | 'after'\n            const rec = processedStore.find(x => x.id === id);\n            if (!rec) return;\n\n            \/\/    'before' ~ front, 'after' ~ back\n            applySelection(which === 'before' ? 'front' : 'back', id);\n\n            \/\/ 2) C\u00e1\u00ba\u00adp nh\u00e1\u00ba\u00adt input file + preview nh\u00c6\u00b0 c\u00c5\u00a9\n            const sourceBlob = await rec.getBlob();\n            const targetName = rec.name.replace(\n                '_fixed.png', which === 'before' ? '_front.jpg' : '_back.jpg'\n            ).replace(\/\\.(png|webp)$\/i, '.jpg');\n            const blob = await cittOptimizeBlobForOcr(sourceBlob, targetName);\n            const file = fileFromBlob(blob, targetName) || blobToFile(blob, targetName);\n\n            const inputEl = document.getElementById(which === 'before' ? 'image-before' : 'image-after');\n            const replaced = replaceInputFile(inputEl, file);\n            setPreview(which === 'before' ? 'preview-before' : 'preview-after', blob);\n\n            const flipWrap = document.getElementById('preview-flip-wrap');\n            if (flipWrap) {\n                flipWrap.dataset.face = (which === 'before') ? 'front' : 'back';\n                syncFlipPreview();\n            }\n\n            \/\/ Qu\u00e1\u00ba\u00a3n l\u00c3\u00bd hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039 form upload\n            const uploadForm = document.getElementById('citt-upload-form');\n            const resultDiv = document.getElementById('citt-result');\n            const resultOcrText = document.querySelector('.ocr-text');\n\n            if (which === 'before') {\n                \/\/ Hi\u00e1\u00bb\u2021n form khi ch\u00e1\u00bb\u008dn frontside\n                if (uploadForm) {\n                    uploadForm.style.display = 'block';\n                }\n                if (resultDiv) {\n                    resultDiv.style.display = 'none';\n                    resultDiv.innerHTML = '';\n                }\n\n                syncSubmitButtonVisibility();\n\n                \/\/ Check n\u00e1\u00ba\u00bfu c\u00e1\u00ba\u00a3 2 preview \u00c4\u2018\u00c3\u00a3 c\u00c3\u00b3 \u00e1\u00ba\u00a3nh th\u00c3\u00ac scroll\n                checkBothPreviewsAndScroll();\n\n            } else if (which === 'after') {\n                \/\/ Hi\u00e1\u00bb\u2021n form khi ch\u00e1\u00bb\u008dn backside\n\n                \/\/ Hi\u00e1\u00bb\u2021n form upload\n                if (uploadForm) {\n                    uploadForm.style.display = 'block';\n                    if (resultOcrText) {\n                        resultOcrText.style.display = 'none';\n                    }\n                } else {\n                }\n                syncSubmitButtonVisibility();\n\n                \/\/ Hi\u00e1\u00bb\u2021n result div\n                if (resultDiv) {\n                    resultDiv.style.display = 'none';\n                    if (resultOcrText) {\n                        resultOcrText.style.display = 'none';\n                    }\n                } else {\n                }\n\n                \/\/ Check n\u00e1\u00ba\u00bfu c\u00e1\u00ba\u00a3 2 preview \u00c4\u2018\u00c3\u00a3 c\u00c3\u00b3 \u00e1\u00ba\u00a3nh th\u00c3\u00ac scroll\n                checkBothPreviewsAndScroll();\n            }\n\n            cittQueueBackgroundOcr();\n        }\n\n        \/\/ H\u00c3\u00a0m ki\u00e1\u00bb\u0192m tra c\u00e1\u00ba\u00a3 2 preview \u00c4\u2018\u00c3\u00a3 c\u00c3\u00b3 \u00e1\u00ba\u00a3nh ch\u00c6\u00b0a, n\u00e1\u00ba\u00bfu c\u00c3\u00b3 th\u00c3\u00ac scroll\n        function checkBothPreviewsAndScroll() {\n            const previewBefore = document.getElementById('preview-before');\n\n            const hasBeforeImage = previewBefore?.querySelector('img');\n\n            \/\/ Ch\u00e1\u00bb\u2030 c\u00e1\u00ba\u00a7n M\u00e1\u00ba\u00b7t tr\u00c6\u00b0\u00e1\u00bb\u203ac (namecard 1 m\u00e1\u00ba\u00b7t ho\u00e1\u00ba\u00b7c 2 m\u00e1\u00ba\u00b7t)\n            if (hasBeforeImage) {\n                \/\/ \u00e2\u0153\u2026 \u00c4\u0090\u00c6\u00a1n gi\u00e1\u00ba\u00a3n h\u00c6\u00a1n\n                setTimeout(() => {\n                    scrollToSubmit();\n                    const submitBtn = document.querySelector('#citt-upload-form button[type=\"submit\"]');\n                    submitBtn?.focus({\n                        preventScroll: true\n                    });\n                }, 100);\n            }\n        }\n\n\n\n        function scrollToSubmit() {\n            const el = document.querySelector('#citt-upload-form');\n            if (!el) return;\n            const y = el.getBoundingClientRect().top + window.pageYOffset - getStickyOffset();\n            window.scrollTo({\n                top: y,\n                behavior: 'smooth'\n            });\n        }\n\n        function haveBothSides() {\n            \/\/ Ch\u00e1\u00bb\u2030 c\u00e1\u00ba\u00a7n M\u00e1\u00ba\u00b7t tr\u00c6\u00b0\u00e1\u00bb\u203ac (namecard 1 m\u00e1\u00ba\u00b7t ho\u00e1\u00ba\u00b7c 2 m\u00e1\u00ba\u00b7t)\n            return !!selectedFrontId;\n        }\n\n\n        function setCropBadge(card, text, bg) {\n            const state = card ? card.querySelector('.crop-state') : null;\n            if (!state) return;\n            state.textContent = text;\n            state.style.background = bg || 'rgba(11,33,69,.85)';\n            state.style.display = 'inline-block';\n        }\n\n        async function openManualCropEditor(card, rec) {\n            if (!rec || typeof rec.getBlob !== 'function') return;\n            const blob = (typeof rec.getOriginalBlob === 'function') ? await rec.getOriginalBlob() : await rec.getBlob();\n            const url = URL.createObjectURL(blob);\n            const img = new Image();\n            img.decoding = 'async';\n            await new Promise((resolve, reject) => {\n                img.onload = resolve;\n                img.onerror = reject;\n                img.src = url;\n            });\n\n            const modal = document.createElement('div');\n            modal.className = 'citt-crop-modal';\n            modal.innerHTML = '<div class=\"citt-crop-panel\"><div class=\"citt-crop-stage\"><canvas><\/canvas><\/div><div class=\"citt-crop-actions\"><button type=\"button\" data-act=\"reset\">Reset<\/button><button type=\"button\" data-act=\"cancel\">Cancel<\/button><button type=\"button\" data-act=\"apply\">Apply<\/button><\/div><\/div>';\n            document.body.appendChild(modal);\n\n            const canvas = modal.querySelector('canvas');\n            const ctx = canvas.getContext('2d');\n            const maxW = Math.min(window.innerWidth - 48, 980);\n            const maxH = Math.min(window.innerHeight - 150, 680);\n            const scale = Math.min(maxW \/ img.naturalWidth, maxH \/ img.naturalHeight, 1);\n            canvas.width = Math.max(1, Math.round(img.naturalWidth * scale));\n            canvas.height = Math.max(1, Math.round(img.naturalHeight * scale));\n\n            const full = () => ({ x: 0, y: 0, w: canvas.width, h: canvas.height });\n            let crop = (() => {\n                const marginX = Math.round(canvas.width * 0.04);\n                const marginY = Math.round(canvas.height * 0.06);\n                return { x: marginX, y: marginY, w: canvas.width - marginX * 2, h: canvas.height - marginY * 2 };\n            })();\n            let drag = null;\n\n            function draw() {\n                ctx.clearRect(0, 0, canvas.width, canvas.height);\n                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n                ctx.save();\n                ctx.fillStyle = 'rgba(0,0,0,.42)';\n                ctx.fillRect(0, 0, canvas.width, crop.y);\n                ctx.fillRect(0, crop.y + crop.h, canvas.width, canvas.height - crop.y - crop.h);\n                ctx.fillRect(0, crop.y, crop.x, crop.h);\n                ctx.fillRect(crop.x + crop.w, crop.y, canvas.width - crop.x - crop.w, crop.h);\n                ctx.strokeStyle = '#00a3ff';\n                ctx.lineWidth = 2;\n                ctx.strokeRect(crop.x, crop.y, crop.w, crop.h);\n                ctx.fillStyle = '#fff';\n                [[crop.x,crop.y],[crop.x+crop.w,crop.y],[crop.x,crop.y+crop.h],[crop.x+crop.w,crop.y+crop.h]].forEach(([x,y]) => {\n                    ctx.fillRect(x - 5, y - 5, 10, 10);\n                    ctx.strokeRect(x - 5, y - 5, 10, 10);\n                });\n                ctx.restore();\n            }\n            function pos(ev) {\n                const p = ev.touches ? ev.touches[0] : ev;\n                const r = canvas.getBoundingClientRect();\n                return { x: p.clientX - r.left, y: p.clientY - r.top };\n            }\n            function hit(p) {\n                const pts = { nw:[crop.x,crop.y], ne:[crop.x+crop.w,crop.y], sw:[crop.x,crop.y+crop.h], se:[crop.x+crop.w,crop.y+crop.h] };\n                for (const [k,[x,y]] of Object.entries(pts)) if (Math.abs(p.x-x) < 18 && Math.abs(p.y-y) < 18) return k;\n                if (p.x >= crop.x && p.x <= crop.x + crop.w && p.y >= crop.y && p.y <= crop.y + crop.h) return 'move';\n                return 'new';\n            }\n            function down(ev) {\n                ev.preventDefault();\n                const p = pos(ev);\n                drag = { mode: hit(p), sx: p.x, sy: p.y, start: { ...crop } };\n                if (drag.mode === 'new') crop = { x: p.x, y: p.y, w: 1, h: 1 };\n                draw();\n            }\n            function move(ev) {\n                if (!drag) return;\n                ev.preventDefault();\n                const p = pos(ev), dx = p.x - drag.sx, dy = p.y - drag.sy;\n                const st = drag.start;\n                if (drag.mode === 'move') {\n                    crop.x = Math.max(0, Math.min(canvas.width - crop.w, st.x + dx));\n                    crop.y = Math.max(0, Math.min(canvas.height - crop.h, st.y + dy));\n                } else if (drag.mode === 'new') {\n                    crop.x = Math.min(drag.sx, p.x); crop.y = Math.min(drag.sy, p.y);\n                    crop.w = Math.abs(dx); crop.h = Math.abs(dy);\n                } else {\n                    let x1 = st.x, y1 = st.y, x2 = st.x + st.w, y2 = st.y + st.h;\n                    if (drag.mode.indexOf('n') !== -1) y1 = st.y + dy;\n                    if (drag.mode.indexOf('s') !== -1) y2 = st.y + st.h + dy;\n                    if (drag.mode.indexOf('w') !== -1) x1 = st.x + dx;\n                    if (drag.mode.indexOf('e') !== -1) x2 = st.x + st.w + dx;\n                    x1 = Math.max(0, Math.min(canvas.width - 20, x1));\n                    y1 = Math.max(0, Math.min(canvas.height - 20, y1));\n                    x2 = Math.max(20, Math.min(canvas.width, x2));\n                    y2 = Math.max(20, Math.min(canvas.height, y2));\n                    crop = { x: Math.min(x1,x2), y: Math.min(y1,y2), w: Math.abs(x2-x1), h: Math.abs(y2-y1) };\n                }\n                crop.w = Math.max(20, Math.min(canvas.width - crop.x, crop.w));\n                crop.h = Math.max(20, Math.min(canvas.height - crop.y, crop.h));\n                draw();\n            }\n            function up() { drag = null; }\n            canvas.addEventListener('mousedown', down);\n            canvas.addEventListener('touchstart', down, { passive: false });\n            window.addEventListener('mousemove', move);\n            window.addEventListener('touchmove', move, { passive: false });\n            window.addEventListener('mouseup', up);\n            window.addEventListener('touchend', up);\n\n            function close() {\n                URL.revokeObjectURL(url);\n                window.removeEventListener('mousemove', move);\n                window.removeEventListener('touchmove', move);\n                window.removeEventListener('mouseup', up);\n                window.removeEventListener('touchend', up);\n                modal.remove();\n            }\n            modal.querySelector('[data-act=\"cancel\"]').onclick = close;\n            modal.querySelector('[data-act=\"reset\"]').onclick = () => { crop = full(); draw(); };\n            modal.querySelector('[data-act=\"apply\"]').onclick = async () => {\n                const sx = Math.round(crop.x \/ scale), sy = Math.round(crop.y \/ scale);\n                const sw = Math.round(crop.w \/ scale), sh = Math.round(crop.h \/ scale);\n                const out = document.createElement('canvas');\n                out.width = Math.max(1, sw); out.height = Math.max(1, sh);\n                out.getContext('2d').drawImage(img, sx, sy, sw, sh, 0, 0, out.width, out.height);\n                const newBlob = await new Promise(r => out.toBlob(r, 'image\/jpeg', 0.95));\n                rec._overrideBlob = newBlob;\n                rec.cropMode = 'manual';\n                rec.getBlob = async () => rec._overrideBlob;\n                rec.getFile = async () => blobToFile(newBlob, (rec.name || 'namecard').replace(\/\\.[^.]+$\/, '_manualcrop.jpg'));\n                await refreshCardThumb(card, rec);\n                setCropBadge(card, 'Manual', 'rgba(19,121,61,.9)');\n                close();\n            };\n            draw();\n        }\n\n        function openImageEditActions(card, rec) {\n            const modal = document.createElement('div');\n            modal.className = 'citt-crop-modal';\n            modal.innerHTML = '<div class=\"citt-edit-image-panel\"><div class=\"citt-edit-image-title\">Edit photo<\/div><div class=\"citt-edit-image-actions\"><button type=\"button\" data-act=\"manual-crop\">Manual crop<\/button><button type=\"button\" data-act=\"cancel\">Cancel<\/button><\/div><\/div>';\n            document.body.appendChild(modal);\n            const close = () => modal.remove();\n            modal.addEventListener('click', (ev) => { if (ev.target === modal) close(); });\n            modal.querySelector('[data-act=\"cancel\"]').addEventListener('click', close);\n            modal.querySelector('[data-act=\"manual-crop\"]').addEventListener('click', async () => {\n                close();\n                await openManualCropEditor(card, rec);\n            });\n        }\n\n        async function refreshCardThumb(card, rec) {\n            const thumb = card.querySelector('.thumb');\n            if (!thumb) return;\n            thumb.querySelectorAll('img:not(.btnDelete img)').forEach(el => el.remove());\n            const b = await rec.getBlob();\n            const url = URL.createObjectURL(b);\n            const img = document.createElement('img');\n            img.src = url;\n            img.alt = rec.name || 'namecard';\n            img.style.display = 'block';\n            img.style.width = '100%';\n            img.style.height = 'auto';\n            img.onload = () => setTimeout(() => URL.revokeObjectURL(url), 30000);\n            thumb.appendChild(img);\n        }\n\n        function cittCurrentLanguagePrefix() {\n            const htmlLang = (document.documentElement.getAttribute('lang') || '').toLowerCase();\n            const path = (window.location && window.location.pathname ? window.location.pathname : '').toLowerCase();\n            if (htmlLang.indexOf('ja') === 0 || path.indexOf('\/ja\/') === 0 || path === '\/ja') return '\/ja';\n            if (htmlLang.indexOf('en') === 0 || path.indexOf('\/en\/') === 0 || path === '\/en') return '\/en';\n            return '';\n        }\n\n        function cittLanguageUrl(path) {\n            const prefix = cittCurrentLanguagePrefix();\n            const cleanPath = String(path || '\/').replace(\/^\\\/+\/, '\/');\n            return prefix + cleanPath;\n        }\n\n        \/\/ Function \u00c4\u2018\u00e1\u00bb\u0192 ki\u00e1\u00bb\u0192m tra v\u00c3\u00a0 redirect khi kh\u00c3\u00b4ng c\u00c3\u00b2n item trong gallery-item\n        function checkAndRedirectIfEmpty() {\n            const galleryItems = document.querySelectorAll('.gallery-item .card');\n            if (galleryItems.length === 0) {\n                const redirectUrl = cittLanguageUrl('\/login\/');\n                setTimeout(() => {\n                    window.location.href = redirectUrl;\n                }, 2000);\n                return true;\n            }\n            return false;\n        }\n\n        \/\/ Function \u00c4\u2018\u00e1\u00bb\u0192 x\u00c3\u00b3a t\u00e1\u00ba\u00a5t c\u00e1\u00ba\u00a3 c\u00c3\u00a1c item trong gallery - \u00c4\u0090\u00c3\u0192 CHUY\u00e1\u00bb\u201aN SANG script.js\n        \/\/ Gi\u00e1\u00bb\u00af l\u00e1\u00ba\u00a1i function r\u00e1\u00bb\u2014ng \u00c4\u2018\u00e1\u00bb\u0192 tr\u00c3\u00a1nh l\u00e1\u00bb\u2014i n\u00e1\u00ba\u00bfu c\u00c3\u00b3 code kh\u00c3\u00a1c g\u00e1\u00bb\u008di\n        function deleteAllGalleryItems() {\n            console.warn(\"deleteAllGalleryItems() \u00c4\u2018\u00c3\u00a3 \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c chuy\u00e1\u00bb\u0192n sang script.js, kh\u00c3\u00b4ng n\u00c3\u00aan g\u00e1\u00bb\u008di t\u00e1\u00bb\u00ab \u00c4\u2018\u00c3\u00a2y\");\n            return;\n        }\n\n        \/\/ Helper: replace card image logic (shared by file input and camera)\n        async function replaceCardImage(blob, rec, card) {\n            try {\n                \/\/ 1. Keep the replacement original for manual crop, then auto-crop the active image.\n                const originalBlob = blob;\n                rec.getOriginalBlob = async () => originalBlob;\n                let sourceBlob = blob;\n                if (blob instanceof File) {\n                    sourceBlob = await maybeAutoCropUploadFile(blob);\n                }\n                const sourceName = sourceBlob.name || \"\";\n                rec.cropMode = sourceBlob.__cittCropMode || (sourceName.indexOf(\"_basiccrop.\") !== -1 ? \"basic\" : (sourceName.indexOf(\"_review.\") !== -1 ? \"review\" : \"\"));\n                const tmpUrl = URL.createObjectURL(sourceBlob);\n                const imgEl = new Image();\n                imgEl.decoding = \"async\";\n                imgEl.loading = \"eager\";\n                await new Promise((res, rej) => {\n                    imgEl.onload = () => res();\n                    imgEl.onerror = (err) => rej(err);\n                    imgEl.src = tmpUrl;\n                });\n\n                \/\/ 2. Render directly. If simple bounds are available, trim them; skip perspective warp.\n                const outCanvas = document.createElement(\"canvas\");\n                outCanvas.width = imgEl.naturalWidth || imgEl.width;\n                outCanvas.height = imgEl.naturalHeight || imgEl.height;\n                const outCtx = outCanvas.getContext(\"2d\");\n                outCtx.imageSmoothingEnabled = true;\n                outCtx.imageSmoothingQuality = \"high\";\n                outCtx.drawImage(imgEl, 0, 0, outCanvas.width, outCanvas.height);\n                if (rec.cropMode !== \"review\") trimCanvasByBasicBounds(outCanvas, 0.06);\n                setTimeout(() => URL.revokeObjectURL(tmpUrl), 30000);\n\n                \/\/ 3. Convert to blob\/file\n                let base = (rec.name || 'card').replace(\/\\.(png|jpg|jpeg|webp)$\/i, '');\n                if (!(\/(_fixed)$\/i.test(base))) base = base.replace(\/\\s*\\(replaced\\)$\/i, '') + '_fixed';\n                const {\n                    blob: newBlob,\n                    file: processedFile,\n                    filename\n                } = await canvasToFile(outCanvas, base + '.png');\n\n                \/\/ 4. Update processedStore record\n                rec._overrideBlob = newBlob;\n                rec.getBlob = async () => rec._overrideBlob || newBlob;\n\n                \/\/ 5. Update Step 1 UI (if exists)\n                const step1Item = document.querySelector(`.item[data-pid=\"${rec.id}\"]`);\n                if (step1Item) {\n                    const step1Canvas = step1Item.querySelector('canvas.out');\n                    if (step1Canvas) {\n                        step1Canvas.width = outCanvas.width;\n                        step1Canvas.height = outCanvas.height;\n                        const s1ctx = step1Canvas.getContext('2d');\n                        s1ctx.clearRect(0, 0, step1Canvas.width, step1Canvas.height);\n                        s1ctx.drawImage(outCanvas, 0, 0);\n                    }\n                    const step1Name = step1Item.querySelector('.name');\n                    if (step1Name) step1Name.textContent = rec.name;\n                }\n\n                \/\/ 6. Update Step 2 Thumbnail\n                const previewUrl = outCanvas.toDataURL('image\/png');\n                const thumb = card.querySelector('.thumb');\n                if (thumb) {\n                    const btnDelete = thumb.querySelector('.btnDelete');\n                    const existingImgs = thumb.querySelectorAll('img:not(.btnDelete img)');\n                    existingImgs.forEach(img => img.remove());\n\n                    const img = document.createElement('img');\n                    img.src = previewUrl;\n                    img.alt = rec.name || 'replaced';\n                    img.style.display = 'block';\n                    img.style.width = '100%';\n                    const state = card.querySelector('.crop-state');\n                    if (state && rec.cropMode === 'review') {\n                        state.textContent = 'Ki\u1ec3m tra';\n                        state.style.display = 'inline-block';\n                    } else if (state && rec.cropMode === 'basic') {\n                        state.textContent = 'Auto';\n                        state.style.display = 'inline-block';\n                    }\n                    img.style.height = 'auto';\n\n                    if (btnDelete) {\n                        btnDelete.insertAdjacentElement('afterend', img);\n                    } else {\n                        thumb.appendChild(img);\n                    }\n                }\n\n                \/\/ 7. Update state\n                if (typeof updateBulkState === 'function') {\n                    try {\n                        updateBulkState();\n                    } catch (_) {}\n                }\n\n            } catch (err) {\n                console.error('Error replacing card image:', err);\n                console.error('Error stack:', err.stack);\n                alert('Kh\u00c3\u00b4ng th\u00e1\u00bb\u0192 x\u00e1\u00bb\u00ad l\u00c3\u00bd \u00e1\u00ba\u00a3nh m\u00e1\u00bb\u203ai. L\u00e1\u00bb\u2014i: ' + (err && err.message ? err.message : String(err)));\n            }\n        }\n\n        \/\/ Helper: Setup update camera for a card\n        function setupUpdateCamera(card, rec) {\n            const startBtn = card.querySelector('.update-take-photo');\n            const camContainer = card.querySelector('.update-camera-container');\n            if (!startBtn || !camContainer) return;\n\n            const video = camContainer.querySelector('.cam-video');\n            const btnCapture = camContainer.querySelector('.cam-capture');\n            const btnCancel = camContainer.querySelector('.cam-cancel');\n            const btnToggle = camContainer.querySelector('.cam-toggle-orient');\n            const guideBox = camContainer.querySelector('.cam-guide-box');\n            const guide = camContainer.querySelector('.cam-guide');\n            let stream = null;\n            let localOrientMode = 'auto';\n            let localLastAutoOrient = 'landscape';\n            let localMirror = false;\n\n            function updateLocalOrientButton() {\n                if (!btnToggle) return;\n                const label = (localOrientMode === 'auto') ? 'AUTO' : (localOrientMode === 'landscape' ? 'Horizontal' : 'Vertical');\n                btnToggle.textContent = label;\n            }\n\n            function chooseLocalAutoOrient() {\n                if (video && video.videoWidth && video.videoHeight) return (video.videoWidth >= video.videoHeight) ?\n                    'landscape' : 'portrait';\n                const r = video ? video.getBoundingClientRect() : {\n                    width: 1,\n                    height: 1\n                };\n                return (r.width >= r.height) ? 'landscape' : 'portrait';\n            }\n\n            function getVideoCoverMetricsFor(videoEl) {\n                const vw = videoEl.videoWidth || 0;\n                const vh = videoEl.videoHeight || 0;\n                const rect = videoEl.getBoundingClientRect();\n                const cw = rect.width || 1;\n                const ch = rect.height || 1;\n                if (!vw || !vh) {\n                    return {\n                        vw: 1,\n                        vh: 1,\n                        cw,\n                        ch,\n                        scale: 1,\n                        offsetX: 0,\n                        offsetY: 0\n                    };\n                }\n                const scale = Math.max(cw \/ vw, ch \/ vh);\n                const drawnW = vw * scale;\n                const drawnH = vh * scale;\n                const offsetX = (cw - drawnW) \/ 2;\n                const offsetY = (ch - drawnH) \/ 2;\n                return {\n                    vw,\n                    vh,\n                    cw,\n                    ch,\n                    scale,\n                    offsetX,\n                    offsetY\n                };\n            }\n\n            function getGuideCropRectInVideoPxLocal() {\n                if (!guideBox || !video) return null;\n                const vb = video.getBoundingClientRect();\n                const gb = guideBox.getBoundingClientRect();\n                const m = getVideoCoverMetricsFor(video);\n\n                const xOnCanvas = (gb.left - vb.left) - m.offsetX;\n                const yOnCanvas = (gb.top - vb.top) - m.offsetY;\n\n                let x = xOnCanvas \/ m.scale;\n                let y = yOnCanvas \/ m.scale;\n                let w = gb.width \/ m.scale;\n                let h = gb.height \/ m.scale;\n\n                x = clamp(x, 0, m.vw);\n                y = clamp(y, 0, m.vh);\n                w = clamp(w, 1, m.vw - x);\n                h = clamp(h, 1, m.vh - y);\n\n                return {\n                    x,\n                    y,\n                    w,\n                    h,\n                    vw: m.vw,\n                    vh: m.vh\n                };\n            }\n\n            function updateLocalGuideSize(forceOrient) {\n                if (!guideBox || !video) return;\n                const rect = video.getBoundingClientRect();\n                const orient = forceOrient || ((localOrientMode === 'auto') ? localLastAutoOrient : localOrientMode);\n                const ratio = (orient === 'portrait') ? (CARD_W_PORTRAIT \/ CARD_H_PORTRAIT) : (CARD_W_LANDSCAPE \/\n                    CARD_H_LANDSCAPE);\n                const maxW = rect.width * 0.75;\n                const maxH = rect.height * 0.75;\n                let w = maxW;\n                let h = w \/ ratio;\n                if (h > maxH) {\n                    h = maxH;\n                    w = h * ratio;\n                }\n                guideBox.style.width = w + 'px';\n                guideBox.style.height = h + 'px';\n                guideBox.style.aspectRatio = 'auto';\n            }\n\n            const stopCam = () => {\n                try {\n                    if (video) video.pause();\n                } catch (_) {}\n                if (stream) {\n                    stream.getTracks().forEach(t => t.stop());\n                    stream = null;\n                }\n                if (video) video.srcObject = null;\n                camContainer.style.display = 'none';\n            };\n\n            startBtn.addEventListener('click', async () => {\n                \/\/ Close other cameras if any\n                document.querySelectorAll('.update-camera-container').forEach(el => {\n                    if (el !== camContainer) el.style.display = 'none';\n                });\n\n                if (camContainer.style.display === 'block') {\n                    stopCam();\n                    return;\n                }\n\n                if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {\n                    alert('Thi\u00e1\u00ba\u00bft b\u00e1\u00bb\u2039 kh\u00c3\u00b4ng h\u00e1\u00bb\u2014 tr\u00e1\u00bb\u00a3 camera.');\n                    return;\n                }\n\n                try {\n                    const bestBackId = await getBestBackCameraId();\n\n                    const videoConstraints = {\n                        facingMode: 'environment',\n                        width: {\n                            min: 1280,\n                            ideal: 1920,\n                            max: 4096\n                        },\n                        height: {\n                            min: 720,\n                            ideal: 1080,\n                            max: 2160\n                        },\n                        aspectRatio: {\n                            ideal: 1.7777777778\n                        }\n                    };\n\n                    if (bestBackId) {\n                        delete videoConstraints.facingMode;\n                        videoConstraints.deviceId = {\n                            exact: bestBackId\n                        };\n                    }\n\n                    stream = await navigator.mediaDevices.getUserMedia({\n                        video: videoConstraints,\n                        audio: false\n                    });\n                    video.srcObject = stream;\n\n                    \/\/ --- T\u00e1\u00bb\u0090I \u00c6\u00afU CHO ANDROID ---\n                    const track = stream.getVideoTracks()[0];\n                    if (track.getCapabilities && track.applyConstraints) {\n                        const capabilities = track.getCapabilities();\n                        const advConstraints = {};\n                        if (capabilities.focusMode && capabilities.focusMode.includes('continuous')) {\n                            advConstraints.focusMode = 'continuous';\n                        }\n                        if (capabilities.imageStabilizationMode && capabilities.imageStabilizationMode.includes('on')) {\n                            advConstraints.imageStabilizationMode = 'on';\n                        }\n                        if (capabilities.zoom) {\n                            advConstraints.zoom = capabilities.zoom.min > 1 ? capabilities.zoom.min : 1;\n                        }\n                        if (Object.keys(advConstraints).length > 0) {\n                            await track.applyConstraints({\n                                advanced: [advConstraints]\n                            });\n                        }\n                    }\n\n                    localMirror = detectMirrorFromStream(stream);\n                    camContainer.style.display = 'block';\n\n                    \/\/ Auto-scroll to camera container for better UX\n                    setTimeout(() => {\n                        camContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });\n                    }, 100);\n\n                    \/\/ CSS now ensures per-card container has same layout as main camera,\n                    \/\/ so updateLocalGuideSize() will compute the same dimensions.\n\n                    \/\/ Reset orient\n                    localOrientMode = 'auto';\n                    localLastAutoOrient = chooseLocalAutoOrient();\n                    updateLocalOrientButton();\n\n                    video.addEventListener('loadedmetadata', () => {\n                        localLastAutoOrient = chooseLocalAutoOrient();\n                        updateLocalGuideSize();\n                    }, {\n                        once: true\n                    });\n\n                    setTimeout(() => updateLocalGuideSize(), 100);\n                    setTimeout(() => updateLocalGuideSize(), 300);\n                } catch (e) {\n                    console.error(e);\n                    setTimeout(() => updateLocalGuideSize(), 600);\n                    alert('Kh\u00c3\u00b4ng th\u00e1\u00bb\u0192 m\u00e1\u00bb\u0178 camera.');\n                }\n            });\n\n            if (btnCancel) {\n                btnCancel.addEventListener('click', stopCam);\n            }\n\n            if (btnToggle) {\n                btnToggle.addEventListener('click', () => {\n                    localOrientMode = (localOrientMode === 'auto') ? 'landscape' : (localOrientMode ===\n                        'landscape' ? 'portrait' : 'auto');\n                    if (localOrientMode === 'auto') localLastAutoOrient = chooseLocalAutoOrient();\n                    updateLocalOrientButton();\n                    updateLocalGuideSize();\n                });\n            }\n\n            if (btnCapture) {\n                btnCapture.addEventListener('click', async () => {\n                    if (!stream || !video.videoWidth) return;\n                    btnCapture.disabled = true;\n                    try {\n                        \/\/ Try auto-crop from video frame using OpenCV (preferable)\n                        const autoRes = (typeof autoCropFromVideoFrame === 'function') ? autoCropFromVideoFrame(\n                            video) : null;\n                        let outCanvas = null;\n                        if (localOrientMode === 'auto' && autoRes && autoRes.canvas) {\n                            outCanvas = autoRes.canvas;\n                            if (autoRes.orientation) {\n                                localLastAutoOrient = autoRes.orientation;\n                                updateLocalGuideSize(localLastAutoOrient);\n                            }\n                        }\n\n                        \/\/ FALLBACK: crop according to guide box view\n                        if (!outCanvas) {\n                            const rect = getGuideCropRectInVideoPxLocal();\n                            if (!rect) throw new Error('Kh\u00c3\u00b4ng l\u00e1\u00ba\u00a5y \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c v\u00c3\u00b9ng crop theo khung.');\n\n                            let sx = rect.x,\n                                sy = rect.y,\n                                sw = rect.w,\n                                sh = rect.h;\n                            if (localMirror) {\n                                sx = Math.max(0, rect.vw - (sx + sw));\n                            }\n\n                            const orient = (localOrientMode === 'auto') ? localLastAutoOrient : localOrientMode;\n                            const outW = (orient === 'portrait') ? CARD_W_PORTRAIT : CARD_W_LANDSCAPE;\n                            const outH = (orient === 'portrait') ? CARD_H_PORTRAIT : CARD_H_LANDSCAPE;\n\n                            outCanvas = document.createElement('canvas');\n                            outCanvas.width = outW;\n                            outCanvas.height = outH;\n                            const ctx = outCanvas.getContext('2d');\n                            ctx.drawImage(video, sx, sy, sw, sh, 0, 0, outW, outH);\n                        }\n\n                        const blob = await new Promise((resolve) => outCanvas.toBlob(resolve, 'image\/jpeg',\n                            0.95));\n                        \/\/ Stop camera ASAP\n                        stopCam();\n                        if (blob) {\n                            await replaceCardImage(blob, rec, card);\n                        }\n                    } catch (err) {\n                        console.error(err);\n                        alert('L\u00e1\u00bb\u2014i ch\u00e1\u00bb\u00a5p \u00e1\u00ba\u00a3nh: ' + (err && err.message ? err.message : err));\n                    } finally {\n                        btnCapture.disabled = false;\n                    }\n                });\n            }\n        }\n\n        function buildPickGallery() {\n            pickGallery.innerHTML = '';\n            if (processedStore.length === 0) {\n                pickGallery.innerHTML = '<div class=\"wrap hint\">Ch&#x1B0;a c&#xF3; &#x1EA3;nh n&#xE0;o - quay l&#x1EA1;i B&#x1B0;&#x1EDB;c 1.<\/div>';\n                \/\/ Sau khi set innerHTML, ki\u00e1\u00bb\u0192m tra l\u00e1\u00ba\u00a1i DOM\n                setTimeout(() => {\n                    checkAndRedirectIfEmpty();\n                }, 100);\n                return;\n            }\n            processedStore.forEach(rec => {\n                const card = document.createElement('div');\n                card.className = 'card';\n                card.dataset.id = rec.id; \/\/ <- \u00c4\u2018\u00e1\u00bb\u0192 map UI\n\n                \/\/ Note: Removed duplicate IDs, used classes instead for camera elements\n                card.innerHTML = `\n      <div class=\"thumb\">\n        <span class=\"crop-state\" style=\"position:absolute;left:8px;top:8px;z-index:3;background:rgba(11,33,69,.85);color:#fff;font-size:11px;padding:3px 6px;border-radius:8px;display:none;\"><\/span>\n        <button class=\"btnDelete danger\" title=\"X\u00c3\u00b3a \u00e1\u00ba\u00a3nh n\u00c3\u00a0y\">\n          <img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/cancel2.png\" alt=\"X\u00c3\u00b3a\">\n        <\/button>\n      <\/div>\n      <!-- tag badge s\u00e1\u00ba\u00bd \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c ch\u00c3\u00a8n \u00c4\u2018\u00e1\u00bb\u2122ng khi ch\u00e1\u00bb\u008dn -->\n      <div class=\"ph\">\n        <button data-act=\"front\" type=\"button\" id=\"frontside\"><img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/namecard.png\" alt=\"Edit\" style=\"width: 24px; height: 24px;\"> Front side<\/button>\n        <button data-act=\"back\" id=\"backside\" type=\"button\"><img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/namecard.png\" alt=\"Edit\" style=\"width: 24px; height: 24px;\"> Back side<\/button>\n        <button type=\"button\" class=\"btn-edit-image\"><img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/edit.svg\" alt=\"Edit\" style=\"width: 24px; height: 24px;\"><span>Edit<\/span><\/button>\n        <button type=\"button\" class=\"btn-replace-cropped\"><img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/edit.svg\" alt=\"Edit\" style=\"width: 24px; height: 24px;\"><span>Change photo<\/span><\/button>\n        <button class=\"update-take-photo\" type=\"button\"><img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/camera.svg\" alt=\"\"> Take Photos<\/button>\n        <input type=\"file\" accept=\"image\/*\" class=\"input-replace-cropped\" style=\"display:none\" \/>\n      <\/div>\n      <div class=\"update-camera-container\" style=\"display:none\">\n            <div class=\"cam-wrap\">\n                <div class=\"height-cam\">\n                    <video class=\"cam-video\" playsinline autoplay muted><\/video>\n                    <!-- Guide overlay for per-card camera -->\n                    <div class=\"cam-guide\" aria-hidden=\"true\">\n                        <div class=\"cam-guide-box\"><\/div>\n                    <\/div>\n                <\/div>\n\n                <div class=\"cam-controls\">\n                    <div class=\"cam-capture-left\">\n                        <button class=\"cam-capture\" type=\"button\"><img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/camera.svg\" alt=\"\">Capture<\/button>\n                        <button class=\"cam-toggle-orient\" type=\"button\" title=\"Toggle frame: AUTO \/ Horizontal \/ Vertical\">AUTO<\/button>\n                        <button class=\"cam-done\" type=\"button\" disabled>&#10004; Use <span class=\"cam-count\">0<\/span> photos<\/button>\n                    <\/div>\n                    <button class=\"cam-cancel\" type=\"button\"><img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/cancel2.png\" alt=\"\"><\/button>\n                <\/div>\n\n                <div class=\"cam-thumbs\" aria-live=\"polite\"><\/div>\n            <\/div>\n        <\/div> \n      `;\n\n                pickGallery.prepend(card);\n\n                \/\/ load thumb\n                (async () => {\n                    const b = await rec.getBlob();\n                    const url = URL.createObjectURL(b);\n                    const img = document.createElement('img');\n                    img.src = url;\n                    img.alt = rec.name;\n                    img.style.display = 'block';\n                    img.style.width = '100%';\n                    img.style.height = 'auto';\n                    img.onload = () => setTimeout(() => URL.revokeObjectURL(url), 30000);\n                    card.querySelector('.thumb').appendChild(img);\n\n                    const stateEl = card.querySelector('.crop-state');\n                    const nm = String(rec.name || '').toLowerCase();\n                    if (stateEl) {\n                        if (rec.cropMode === 'review') {\n                            stateEl.textContent = 'Ki\u1ec3m tra';\n                            stateEl.style.background = 'rgba(132,28,28,.85)';\n                            stateEl.style.display = 'inline-block';\n                        } else if (rec.cropMode === 'basic') {\n                            stateEl.textContent = 'Auto';\n                            stateEl.style.background = 'rgba(180,115,0,.9)';\n                            stateEl.style.display = 'inline-block';\n                        }\n                    }\n                })();\n\n                \/\/ Click handlers\n                card.querySelector('[data-act=\"front\"]').addEventListener('click', () => assignTo('before', rec\n                    .id));\n                card.querySelector('[data-act=\"back\"]').addEventListener('click', () => assignTo('after', rec.id));\n\n                \/\/ Setup Camera\n                setupUpdateCamera(card, rec);\n\n                \/\/ Note: button to use main camera removed; per-card camera will be used instead.\n\n                \/\/ Delete button handler\n                const btnDelete = card.querySelector('.btnDelete');\n                if (btnDelete) {\n                    btnDelete.addEventListener('click', () => {\n                        \/\/ X\u00c3\u00b3a kh\u00e1\u00bb\u008fi selection n\u00e1\u00ba\u00bfu \u00c4\u2018ang \u00c4\u2018\u00c6\u00b0\u00e1\u00bb\u00a3c ch\u00e1\u00bb\u008dn\n                        if (selectedFrontId === rec.id) {\n                            selectedFrontId = null;\n                            \/\/ Clear preview v\u00c3\u00a0 input file cho front\n                            const previewBefore = document.getElementById('preview-before');\n                            if (previewBefore) {\n                                previewBefore.innerHTML = '<div class=\"hint\">No front side selected<\/div>';\n                            }\n                            const inputBefore = document.getElementById('image-before');\n                            if (inputBefore) {\n                                const dt = new DataTransfer();\n                                const newInput = document.createElement('input');\n                                newInput.type = 'file';\n                                newInput.name = inputBefore.name;\n                                newInput.id = inputBefore.id;\n                                newInput.accept = inputBefore.accept;\n                                newInput.required = inputBefore.required;\n                                newInput.style.display = 'none';\n                                newInput.files = dt.files;\n                                inputBefore.parentNode.insertBefore(newInput, inputBefore);\n                                inputBefore.remove();\n                            }\n                        }\n                        if (selectedBackId === rec.id) {\n                            selectedBackId = null;\n                            \/\/ Clear preview v\u00c3\u00a0 input file cho back\n                            const previewAfter = document.getElementById('preview-after');\n                            if (previewAfter) {\n                                previewAfter.innerHTML = '<div class=\"hint\">No back side selected<\/div>';\n                            }\n                            const inputAfter = document.getElementById('image-after');\n                            if (inputAfter) {\n                                const dt = new DataTransfer();\n                                const newInput = document.createElement('input');\n                                newInput.type = 'file';\n                                newInput.name = inputAfter.name;\n                                newInput.id = inputAfter.id;\n                                newInput.accept = inputAfter.accept;\n                                newInput.required = inputAfter.required;\n                                newInput.style.display = 'none';\n                                newInput.files = dt.files;\n                                inputAfter.parentNode.insertBefore(newInput, inputAfter);\n                                inputAfter.remove();\n                            }\n                            \/\/ \u00e1\u00ba\u00a8n form n\u00e1\u00ba\u00bfu \u00c4\u2018ang hi\u00e1\u00bb\u0192n th\u00e1\u00bb\u2039\n                            const uploadForm = document.getElementById('citt-upload-form');\n                            if (uploadForm) {\n                                uploadForm.style.display = 'none';\n                            }\n                        }\n\n                        \/\/ X\u00c3\u00b3a item t\u00c6\u00b0\u00c6\u00a1ng \u00e1\u00bb\u00a9ng trong grid (stage A)\n                        removeGridItemById(rec.id);\n\n                        \/\/ X\u00c3\u00b3a kh\u00e1\u00bb\u008fi processedStore\n                        const index = processedStore.findIndex(x => x.id === rec.id);\n                        if (index !== -1) {\n                            processedStore.splice(index, 1);\n                        }\n\n                        \/\/ X\u00c3\u00b3a card kh\u00e1\u00bb\u008fi DOM\n                        card.remove();\n\n                        \/\/ Ki\u00e1\u00bb\u0192m tra v\u00c3\u00a0 redirect n\u00e1\u00ba\u00bfu kh\u00c3\u00b4ng c\u00c3\u00b2n item\n                        checkAndRedirectIfEmpty();\n\n                        \/\/ C\u00e1\u00ba\u00adp nh\u00e1\u00ba\u00adt state\n                        if (typeof updateBulkState === 'function') {\n                            updateBulkState();\n                        }\n                    });\n                }\n\n                \/\/ Image edit actions and replacement file input\n                const editImageBtn = card.querySelector('.btn-edit-image');\n                const replaceBtn = card.querySelector('.btn-replace-cropped');\n                const replaceInput = card.querySelector('.input-replace-cropped');\n\n                if (editImageBtn) {\n                    editImageBtn.addEventListener('click', () => openImageEditActions(card, rec));\n                }\n\n                if (replaceBtn && replaceInput) {\n                    replaceBtn.addEventListener('click', () => replaceInput.click());\n\n                    replaceInput.addEventListener('change', async (e) => {\n                        const file = e.target.files && e.target.files[0];\n                        if (!file) return;\n                        if (!file.type.startsWith('image\/')) {\n                            alert('Vui l\u00f2ng ch\u1ecdn t\u1ec7p h\u00ecnh \u1ea3nh.');\n                            return;\n                        }\n                        await replaceCardImage(file, rec, card);\n                        e.target.value = '';\n                    });\n                }\n            });\n        }\n\n\n        \/\/ ====== B form submit demo (you can hook to your OCR backend here) ======\n        \/\/ document.getElementById('citt-upload-form').addEventListener('submit', async function(e) {\n        \/\/     e.preventDefault();\n        \/\/     const fd = new FormData(this);\n        \/\/     const out = [];\n        \/\/     for (const [k, v] of fd.entries()) {\n        \/\/         if (v instanceof File) {\n        \/\/             out.push(k + ': ' + v.name + ' (' + v.type + ', ' + v.size + 'B)');\n        \/\/         } else {\n        \/\/             out.push(k + ': ' + v);\n        \/\/         }\n        \/\/     }\n        \/\/     document.getElementById('citt-result').style.display = 'block';\n        \/\/     document.getElementById('citt-result').innerText = '\u00c4\u0090\u00c3\u00a3 gom form (demo):\\n' + out.join('\\n');\n\n        \/\/ });\n\n\n        \/\/ Initialize step 1\n        setStep(1);\n        \/\/ updateBulkState();\n    <\/script>\n<\/body>\n\n<\/html>\n<div id=\"factory-sub\" style=\"display:none;\">\n    <input type=\"checkbox\" name=\"sub_industry[]\" value=\"Manufacturer\"> Nh\u00c3\u00a0 s\u00e1\u00ba\u00a3n xu\u00e1\u00ba\u00a5t\n    <input type=\"checkbox\" name=\"sub_industry[]\" value=\"Manufacturing Trading\"> S\u00e1\u00ba\u00a3n xu\u00e1\u00ba\u00a5t - Th\u00c6\u00b0\u00c6\u00a1ng m\u00e1\u00ba\u00a1i\n<\/div>\n<\/div>\n\n<\/form>\n\n<!-- <div id=\"citt-result\" class=\"wrap test\"><\/div> -->\n<\/section>\n<script>\n    document.getElementById('country').addEventListener('change', function() {\n        document.getElementById('region-select').style.display = this.value === 'Vietnam' ? 'block' : 'none';\n    });\n    document.addEventListener('DOMContentLoaded', function() {\n        const industryCheckboxes = document.querySelectorAll('input[name=\"industry[]\"]');\n        const subOptionsWrapper = document.getElementById('sub-options');\n        industryCheckboxes.forEach(function(checkbox) {\n            checkbox.addEventListener('change', function() {\n                const showMedical = document.querySelector(\n                    'input[name=\"industry[]\"][value=\"Medical\"]').checked;\n                const showFactory = document.querySelector(\n                    'input[name=\"industry[]\"][value=\"Factory\"]').checked;\n                const showAutomobile = document.querySelector(\n                    'input[name=\"industry[]\"][value=\"Automobile\"]').checked;\n                const showTechnology = document.querySelector(\n                    'input[name=\"industry[]\"][value=\"Technology\"]').checked;\n                document.getElementById('medical-sub').style.display = showMedical ? 'block' :\n                    'none';\n                document.getElementById('factory-sub').style.display = showFactory ? 'block' :\n                    'none';\n                const autoSub = document.getElementById('automobile-sub');\n                if (autoSub) autoSub.style.display = showAutomobile ? 'block' : 'none';\n                const techSub = document.getElementById('technology-sub');\n                if (techSub) techSub.style.display = showTechnology ? 'block' : 'none';\n                subOptionsWrapper.style.display = (showMedical || showFactory ||\n                    showAutomobile || showTechnology) ? 'block' : 'none';\n            });\n        });\n\n        const supplierSelect = document.getElementById('supplier_type');\n        const supplierOptionsWrapper = document.getElementById('supplier-options');\n        const businessOptions = document.getElementById('business-options');\n        const serviceOptions = document.getElementById('service-options');\n        const industryWrapper = document.getElementById('industry-wrapper');\n\n\n\n        const businessCheckboxes = document.querySelectorAll('#business-options input[type=\"checkbox\"]');\n        businessCheckboxes.forEach(function(checkbox) {\n            checkbox.addEventListener('change', function() {\n                let anyChecked = Array.from(businessCheckboxes).some(cb => cb.checked);\n                if (supplierSelect.value === 'Business') {\n                    industryWrapper.style.display = anyChecked ? 'block' : 'none';\n                }\n            });\n        });\n    });\n\n    \/\/ ====== B form submit - POST th\u00e1\u00bb\u00b1c l\u00c3\u00aan PHP handler ======\n    const cittResolveAjaxUrl = () => {\n        const fallback = (window.location && window.location.origin ? window.location.origin : '') + '\/wp-admin\/admin-ajax.php';\n        const raw = (typeof citt_ajax !== 'undefined' && citt_ajax && citt_ajax.ajax_url) ? String(citt_ajax.ajax_url).trim() : '';\n        if (!raw) return fallback;\n        try {\n            return new URL(raw, window.location.href).toString();\n        } catch (urlErr) {\n            console.warn('[CITT] Invalid ajax_url, using fallback:', raw, urlErr);\n            return fallback;\n        }\n    };\n\n    let cittBackgroundOcrJob = null;\n    let cittBackgroundOcrTimer = null;\n    const cittDataUrlCache = new Map();\n\n    function cittGetOcrFileSignature(form) {\n        const parts = [];\n        ['citt_image_before', 'citt_image'].forEach(function(name) {\n            const input = form && form.querySelector('[name=\"' + name + '\"]');\n            const file = input && input.files && input.files[0];\n            parts.push(file ? [name, file.name, file.size, file.lastModified].join(':') : name + ':empty');\n        });\n        return parts.join('|');\n    }\n\n    function cittFileToDataUrl(file) {\n        return new Promise(function(resolve, reject) {\n            const reader = new FileReader();\n            reader.onload = function() { resolve(String(reader.result || '')); };\n            reader.onerror = function() { reject(reader.error || new Error('Cannot read OCR image')); };\n            reader.readAsDataURL(file);\n        });\n    }\n\n    async function cittCachedFileToDataUrl(file) {\n        const key = file ? [file.name, file.size, file.lastModified].join(':') : '';\n        if (!key) return '';\n        if (cittDataUrlCache.has(key)) {\n            return cittDataUrlCache.get(key);\n        }\n        const dataUrl = await cittFileToDataUrl(file);\n        cittDataUrlCache.set(key, dataUrl);\n        return dataUrl;\n    }\n\n    async function cittBuildOcrFormData(form) {\n        const fd = new URLSearchParams();\n        fd.append('action', 'citt_upload_image');\n        fd.append('nonce', (typeof citt_ajax !== 'undefined' ? citt_ajax.nonce : ''));\n\n        const front = form && form.querySelector('[name=\"citt_image_before\"]');\n        const frontFile = front && front.files && front.files[0];\n        if (frontFile) {\n            fd.append('citt_image_before_data', await cittCachedFileToDataUrl(frontFile));\n            fd.append('citt_image_before_name', frontFile.name || 'namecard.jpg');\n        }\n\n        const back = form && form.querySelector('[name=\"citt_image\"]');\n        const backFile = back && back.files && back.files[0];\n        if (backFile) {\n            fd.append('citt_image_data', await cittCachedFileToDataUrl(backFile));\n            fd.append('citt_image_name', backFile.name || 'namecard-back.jpg');\n        }\n        return fd;\n    }\n\n    async function cittPostOcrFormData(fd) {\n        const ajaxUrl = cittResolveAjaxUrl();\n        const ajaxCandidates = [];\n        if (typeof citt_ajax !== 'undefined' && citt_ajax && citt_ajax.ajax_url) {\n            ajaxCandidates.push(String(citt_ajax.ajax_url).trim());\n        }\n        const origin = (window.location && window.location.origin) ? window.location.origin : '';\n        ajaxCandidates.push(ajaxUrl);\n        ajaxCandidates.push(origin + '\/wp-admin\/admin-ajax.php');\n        ajaxCandidates.push('\/wp-admin\/admin-ajax.php');\n\n        let response = null;\n        let lastErr = null;\n        let usedAjaxUrl = '';\n        for (const candidate of ajaxCandidates.filter(Boolean)) {\n            try {\n                response = await fetch(candidate, { method: 'POST', body: fd, credentials: 'same-origin' });\n                usedAjaxUrl = candidate;\n                if (response) break;\n            } catch (e) {\n                lastErr = e;\n            }\n        }\n        if (!response) {\n            throw (lastErr || new Error('Kh\u00f4ng th\u1ec3 k\u1ebft n\u1ed1i endpoint AJAX'));\n        }\n\n        const rawText = await response.text();\n        const rawNormalized = String(rawText || '').replace(\/^\\uFEFF+\/, '').trim();\n        let payloadText = rawNormalized;\n        if (payloadText && payloadText[0] !== '{' && payloadText[0] !== '[') {\n            const jsonStart = payloadText.indexOf('{');\n            const jsonEnd = payloadText.lastIndexOf('}');\n            if (jsonStart !== -1 && jsonEnd > jsonStart) {\n                payloadText = payloadText.slice(jsonStart, jsonEnd + 1);\n            }\n        }\n\n        try {\n            return JSON.parse(payloadText);\n        } catch (parseErr) {\n            console.warn('[CITT] Invalid JSON from', usedAjaxUrl, rawText);\n            throw new Error('Ph\u1ea3n h\u1ed3i m\u00e1y ch\u1ee7 kh\u00f4ng \u0111\u00fang \u0111\u1ecbnh d\u1ea1ng JSON');\n        }\n    }\n\n    function cittGetMatchingBackgroundOcr(form) {\n        const key = cittGetOcrFileSignature(form);\n        if (!cittBackgroundOcrJob || cittBackgroundOcrJob.key !== key) return null;\n        return cittBackgroundOcrJob;\n    }\n\n    function cittQueueBackgroundOcr() {\n        window.clearTimeout(cittBackgroundOcrTimer);\n        cittBackgroundOcrTimer = window.setTimeout(async function() {\n            const form = document.getElementById('citt-upload-form');\n            if (!form) return;\n            const front = form.querySelector('[name=\"citt_image_before\"]');\n            const frontFile = front && front.files && front.files[0];\n            if (!frontFile) return;\n\n            const key = cittGetOcrFileSignature(form);\n            if (cittBackgroundOcrJob && cittBackgroundOcrJob.key === key) return;\n\n            const fd = await cittBuildOcrFormData(form);\n            const startedAt = Date.now();\n            const promise = cittPostOcrFormData(fd)\n                .then(function(json) {\n                    return json;\n                })\n                .catch(function(err) {\n                    console.warn('[CITT] Background OCR failed; submit will retry.', err);\n                    throw err;\n                });\n            cittBackgroundOcrJob = { key: key, promise: promise, startedAt: startedAt };\n        }, 350);\n    }\n\n\n    function cittEscapeHtml(value) {\n        return String(value || '').replace(\/[&<>\"]\/g, function(ch) {\n            return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' })[ch];\n        });\n    }\n\n    function cittIconForOcrLine(line) {\n        const clean = String(line || '')\n            .replace(\/^[\\u{1F300}-\\u{1FAFF}\\u2600-\\u27BF]\\uFE0F?\\s*\/u, '')\n            .replace(\/^[\\?\\u200D\\uFE0F\\s]+\/u, '')\n            .replace(\/^\\[(Email|\u0110\u1ecba ch\u1ec9|Address)\\]\\s*\/u, '');\n        const iconMap = [\n            [\/^(H\u1ecd t\u00ean|Name|\u6c0f\u540d):\/i, 'fa-user'],\n            [\/^(Ch\u1ee9c v\u1ee5|Position|\u5f79\u8077):\/i, 'fa-id-badge'],\n            [\/^(C\u00f4ng ty|Company|\u4f1a\u793e\u540d):\/i, 'fa-building'],\n            [\/^(Email|\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9):\/i, 'fa-envelope'],\n            [\/^(S\u1ed1 \u0111i\u1ec7n tho\u1ea1i|Phone Number|\u96fb\u8a71\u756a\u53f7):\/i, 'fa-phone'],\n            [\/^(Website|\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8):\/i, 'fa-globe'],\n            [\/^(M\u00e3 s\u1ed1 thu\u1ebf|Tax Code|\u7d0d\u7a0e\u8005\u756a\u53f7|\u7a0e\u30b3\u30fc\u30c9):\/i, 'fa-file-text-o'],\n            [\/^(\u0110\u1ecba ch\u1ec9|Address|\u4f4f\u6240):\/i, 'fa-map-marker']\n        ];\n        for (const pair of iconMap) {\n            if (pair[0].test(clean)) {\n                return '<i class=\"fa ' + pair[1] + '\" aria-hidden=\"true\" style=\"width:18px;display:inline-block;margin-right:6px;color:#132441;text-align:center;\"><\/i>' + cittEscapeHtml(clean);\n            }\n        }\n        return cittEscapeHtml(clean);\n    }\n\n    function cittOcrTextToFaHtml(text) {\n        return cittCleanOcrText(text).split(\/\\r?\\n\/).map(function(line) {\n            if (!line.trim()) return '<br>';\n            return '<div>' + cittIconForOcrLine(line) + '<\/div>';\n        }).join('');\n    }\n\n    function cittCleanOcrText(text) {\n        var result = String(text || '');\n        result = result.split('\u7d0d\u7a0e\u8005\u756a\u53f7').join('\u7a0e\u30b3\u30fc\u30c9');\n        return result\n            .replace(\/\u540d{3,}\/g, '\u540d')\n            .replace(\/\u6c0f\u540d{2,}\/g, '\u6c0f\u540d')\n            .replace(\/\u540d\u524d{2,}\/g, '\u540d\u524d');\n    }\n\n    function cittSyncRichOcrText() {\n        const rich = document.getElementById('save-edited-rich');\n        const textarea = document.getElementById('save-edited-text');\n        if (rich && textarea) textarea.value = cittCleanOcrText(rich.innerText).replace(\/\\n{3,}\/g, '\\n\\n').trim();\n    }\n    document.getElementById('citt-upload-form').addEventListener('submit', async function(e) {\n        e.preventDefault();\n\n                \/\/ NKD FIX (2026-04-17):\n        \/\/ Only skip when a dedicated mobile jQuery submit handler is explicitly present.\n        \/\/ Otherwise this inline handler must run on mobile to keep \"Tai len & Chuyen text\" working.\n        if (window.innerWidth <= 768 && window.__cittUploadSubmitHandledByJQ === true) {\n            return;\n        }\n        \/\/ Tr\u00c3\u00aan mobile (\u00e2\u2030\u00a4768px), script.js jQuery handler \u00c4\u2018\u00c3\u00a3 x\u00e1\u00bb\u00ad l\u00c3\u00bd submit\n        \/\/ Skip handler n\u00c3\u00a0y \u00c4\u2018\u00e1\u00bb\u0192 tr\u00c3\u00a1nh 2 AJAX calls song song g\u00c3\u00a2y m\u00e1\u00ba\u00a5t duplicate warning\n        const resultDiv = document.getElementById('citt-result');\n        const theForm   = this;\n        const submitBtn = this.querySelector('button[type=\"submit\"]');\n        if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Processing...'; }\n        resultDiv.style.display = 'block';\n\n        let convertProgressTimer = null;\n        let convertProgressValue = 8;\n        const renderConvertProgress = () => {\n            resultDiv.innerHTML = `\n                <div id=\"citt-convert-status\">\n                    <p style=\"color:#0f7a2f;font-weight:600;margin:0 0 10px 0;\">Uploading &amp; recognizing text...<\/p>\n                    <div style=\"height:8px;background:#d9e3ef;border-radius:999px;overflow:hidden;\">\n                        <div id=\"citt-convert-progress\" style=\"height:100%;width:${convertProgressValue}%;background:linear-gradient(90deg,#0f7a2f,#2aa54f);transition:width .25s ease;\"><\/div>\n                    <\/div>\n                    <p id=\"citt-convert-percent\" style=\"margin:8px 0 0 0;color:#275b34;font-size:13px;\">${convertProgressValue}%<\/p>\n                <\/div>\n            `;\n        };\n        const startConvertProgress = () => {\n            clearInterval(convertProgressTimer);\n            convertProgressValue = 8;\n            renderConvertProgress();\n            convertProgressTimer = setInterval(() => {\n                if (convertProgressValue >= 90) return;\n                convertProgressValue += (convertProgressValue < 40) ? 7 : 3;\n                const bar = document.getElementById('citt-convert-progress');\n                const pct = document.getElementById('citt-convert-percent');\n                if (bar) bar.style.width = convertProgressValue + '%';\n                if (pct) pct.textContent = convertProgressValue + '%';\n            }, 220);\n        };\n        const stopConvertProgress = (finalValue = 100) => {\n            clearInterval(convertProgressTimer);\n            convertProgressValue = finalValue;\n            const bar = document.getElementById('citt-convert-progress');\n            const pct = document.getElementById('citt-convert-percent');\n            if (bar) bar.style.width = convertProgressValue + '%';\n            if (pct) pct.textContent = convertProgressValue + '%';\n        };\n\n        startConvertProgress();\n\n        const fd = await cittBuildOcrFormData(this);\n\n        try {\n            let json;\n            const bgJob = cittGetMatchingBackgroundOcr(this);\n            if (bgJob) {\n                json = await bgJob.promise.catch(async function(err) {\n                    console.warn('[CITT] Background OCR failed during submit; retrying foreground.', err);\n                    return await cittPostOcrFormData(fd);\n                });\n            } else {\n                json = await cittPostOcrFormData(fd);\n            }\n            stopConvertProgress(100);\n\n            if (json.success) {\n                const d = json.data;\n                let statusHtml = '';\n                let forceSaveNote = '';\n                if (d.is_duplicate || d.is_duplicate_confirmable) {\n                    statusHtml = '<p style=\"color:#c81e1e;font-weight:800;background:#ffe9e9;border:1px solid #f5b5b5;border-radius:8px;padding:10px 12px;margin:0 0 12px 0;max-width:100%;overflow-wrap:anywhere;\">&#9888;&#65039; This photo was processed before (' + Math.round((d.similar_percent_raw || 100)) + '% match). Recognized content:<\/p>';\n                } else {\n                    statusHtml = '<p style=\"color:green;font-weight:600;margin:0 0 12px 0;\">\u2705 Recognition successful!<\/p>';\n                }\n\n                \/\/ L\u00e1\u00ba\u00a5y gi\u00c3\u00a1 tr\u00e1\u00bb\u2039 country\/industry \u00c4\u2018\u00c3\u00a3 ch\u00e1\u00bb\u008dn trong form\n                const countryVal  = theForm.querySelector('[name=\"country\"]')?.value || '';\n                const regionVal   = theForm.querySelector('[name=\"region\"]')?.value || '';\n                const supplierVal = theForm.querySelector('[name=\"supplier_type\"]')?.value || '';\n\n                \/\/ Ghi l\u00e1\u00ba\u00a1i raw OCR text v\u00c3\u00a0 image URL \u00c4\u2018\u00e1\u00bb\u0192 d\u00c3\u00b9ng khi l\u00c6\u00b0u\n                const rawText   = cittCleanOcrText(d.raw_ocr_text || d.text || '');\n                const editedText= cittCleanOcrText(d.text || '');\n                const imageUrl  = d.image_url || '';\n                const imageUrlBefore = d.image_url_before || '';\n\n                \/\/ Build save section\n                const saveSection = `\n                <div id=\"citt-save-section\" data-no-translation translate=\"no\" style=\"width:100%;max-width:100%;margin-top:18px;padding:16px;background:#f8f9fa;border-radius:10px;border:1px solid #e0e0e0;overflow:hidden;\">\n                    <p style=\"font-weight:600;margin:0 0 10px 0;\">&#128203; Select information to save:<\/p>\n\n                    <div style=\"margin-bottom:14px;\">\n                        <label style=\"display:block;font-size:13px;margin-bottom:4px;font-weight:500;\">Edit text (if needed)<\/label>\n                        <div id=\"save-edited-rich\" data-no-translation translate=\"no\" contenteditable=\"true\" style=\"display:block;width:100%;max-width:100%;min-height:170px;height:220px;max-height:220px;padding:8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;line-height:1.55;white-space:pre-wrap;overflow-y:auto;overflow-x:hidden;overflow-wrap:anywhere;word-break:break-word;\">${cittOcrTextToFaHtml(editedText)}<\/div>\n                        <textarea id=\"save-edited-text\" data-no-translation translate=\"no\" rows=\"4\" style=\"display:none;\">${cittEscapeHtml(editedText)}<\/textarea>\n                    <\/div>\n                    <input type=\"hidden\" id=\"save-image-url\" value=\"${imageUrl}\">\n                    <input type=\"hidden\" id=\"save-image-url-before\" value=\"${imageUrlBefore}\">\n                    <input type=\"hidden\" id=\"save-raw-ocr\" value=\"${rawText.replace(\/\"\/g,'&quot;')}\">\n                    <div style=\"display:flex;gap:8px;align-items:stretch;\">\n                        <button id=\"btn-luu-namecard\" type=\"button\" style=\"flex:1;background:#132441;color:#fff;padding:10px 8px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;text-align:center;white-space:nowrap;\">\ud83d\udcbe Save Name Card<\/button>\n                        <button id=\"btn-cancel-save\" type=\"button\" style=\"flex:1;background:#e0e0e0;color:#333;padding:10px 8px;border:none;border-radius:8px;font-size:13px;cursor:pointer;text-align:center;white-space:nowrap;\">Cancel<\/button>\n                    <\/div>\n                    <p id=\"save-status\" style=\"margin-top:8px;font-size:13px;\"><\/p>\n                <\/div>`;\n\n                resultDiv.innerHTML = statusHtml + forceSaveNote\n                    + saveSection;\n\n                \/\/ Auto-scroll to Save Name Card button after OCR save\n                setTimeout(function() {\n                    var btnSave = document.getElementById('btn-luu-namecard');\n                    if (btnSave) {\n                        btnSave.scrollIntoView({ behavior: 'smooth', block: 'center' });\n                    }\n                }, 300);\n\n                const richOcr = document.getElementById('save-edited-rich');\n                if (richOcr) richOcr.addEventListener('input', cittSyncRichOcrText);\n\n                \/\/ N\u00c3\u00bat L\u00c6\u00b0u\n                document.getElementById('btn-luu-namecard').addEventListener('click', async () => {\n                    const saveStatus = document.getElementById('save-status');\n                    const btnLuu = document.getElementById('btn-luu-namecard');\n                    btnLuu.disabled = true;\n                    btnLuu.textContent = 'Saving...';\n                    saveStatus.textContent = '';\n\n                    const sf = new URLSearchParams();\n                    sf.append('action', 'citt_save_text');\n                    sf.append('nonce', (typeof citt_ajax !== 'undefined' ? citt_ajax.nonce : ''));\n                    sf.append('image_url', document.getElementById('save-image-url').value);\n                    sf.append('image_url_before', document.getElementById('save-image-url-before').value);\n                    const frontInputForSave = theForm.querySelector('[name=\"citt_image_before\"]');\n                    const backInputForSave = theForm.querySelector('[name=\"citt_image\"]');\n                    if (frontInputForSave && frontInputForSave.files && frontInputForSave.files[0]) {\n                        sf.append('citt_image_before_data', await cittCachedFileToDataUrl(frontInputForSave.files[0]));\n                        sf.append('citt_image_before_name', frontInputForSave.files[0].name || 'namecard-front.jpg');\n                    }\n                    if (backInputForSave && backInputForSave.files && backInputForSave.files[0]) {\n                        sf.append('citt_image_data', await cittCachedFileToDataUrl(backInputForSave.files[0]));\n                        sf.append('citt_image_name', backInputForSave.files[0].name || 'namecard-back.jpg');\n                    }\n                    cittSyncRichOcrText();\n                    sf.append('edited_text', document.getElementById('save-edited-text').value);\n                    sf.append('raw_ocr_text', document.getElementById('save-raw-ocr').value);\n                    sf.append('country', countryVal);\n                    sf.append('supplier_type', supplierVal);\n\n                    \/\/ Collect checked industries from original form\n                    theForm.querySelectorAll('input[name=\"industry[]\"]:checked').forEach(cb => sf.append('industry[]', cb.value));\n                    theForm.querySelectorAll('input[name=\"sub_industry[]\"]:checked').forEach(cb => sf.append('sub_industry[]', cb.value));\n                    theForm.querySelectorAll('input[name=\"business_option[]\"]:checked').forEach(cb => sf.append('business_option[]', cb.value));\n                    theForm.querySelectorAll('input[name=\"service_option[]\"]:checked').forEach(cb => sf.append('service_option[]', cb.value));\n\n                    try {\n                        let sr = null;\n                        let sj = null;\n                        let lastSaveErr = null;\n                        let usedSaveUrl = '';\n                        const saveCandidates = [];\n                        if (typeof citt_ajax !== 'undefined' && citt_ajax && citt_ajax.ajax_url) {\n                            saveCandidates.push(String(citt_ajax.ajax_url).trim());\n                        }\n                        const saveOrigin = (window.location && window.location.origin) ? window.location.origin : '';\n                        saveCandidates.push(cittResolveAjaxUrl());\n                        saveCandidates.push(saveOrigin + '\/wp-admin\/admin-ajax.php');\n                        saveCandidates.push('\/wp-admin\/admin-ajax.php');\n\n                        for (const candidate of saveCandidates.filter(Boolean)) {\n                            try {\n                                sr = await fetch(candidate, { method: 'POST', body: sf, credentials: 'same-origin' });\n                                usedSaveUrl = candidate;\n                                if (sr) break;\n                            } catch (e) {\n                                lastSaveErr = e;\n                            }\n                        }\n\n                        if (!sr) {\n                            throw (lastSaveErr || new Error('Khong the ket noi endpoint AJAX'));\n                        }\n\n                        const saveRaw = await sr.text();\n                        let savePayload = String(saveRaw || '').replace(\/^\\uFEFF+\/, '').trim();\n                        if (savePayload && savePayload[0] !== '{' && savePayload[0] !== '[') {\n                            const jsonStart = savePayload.indexOf('{');\n                            const jsonEnd = savePayload.lastIndexOf('}');\n                            if (jsonStart !== -1 && jsonEnd > jsonStart) {\n                                savePayload = savePayload.slice(jsonStart, jsonEnd + 1);\n                            }\n                        }\n\n                        try {\n                            sj = JSON.parse(savePayload);\n                        } catch (parseErr) {\n                            console.warn('[CITT] Invalid JSON on save from', usedSaveUrl, saveRaw);\n                            throw new Error('Ph\u1ea3n h\u1ed3i m\u00e1y ch\u1ee7 kh\u00f4ng \u0111\u00fang \u0111\u1ecbnh d\u1ea1ng JSON');\n                        }\n                        if (sj.success) {\n                            saveStatus.innerHTML = '<span style=\"color:green;\">' + \"\\u2705 Saved successfully!\" + '<\/span>';\n                            btnLuu.style.display = 'none';\n\n                            \/\/ Auto x\u00c3\u00b3a c\u00c3\u00a1c card \u00c4\u2018\u00c3\u00a3 ch\u00e1\u00bb\u008dn kh\u00e1\u00bb\u008fi h\u00c3\u00a0ng \u00c4\u2018\u00e1\u00bb\u00a3i sau 1.5s\n                            \/\/ Capture IDs ngay l\u00c3\u00bac n\u00c3\u00a0y (tr\u00c3\u00a1nh b\u00e1\u00bb\u2039 null do reassign)\n                            const savedFrontId = selectedFrontId;\n                            const savedBackId = selectedBackId;\n\n                            setTimeout(() => {\n                                \/\/ X\u00c3\u00b3a card front kh\u00e1\u00bb\u008fi gallery\n                                if (savedFrontId) {\n                                    const frontCard = document.querySelector('#pick-gallery-item .card[data-id=\"' + savedFrontId + '\"]');\n                                    if (frontCard) frontCard.remove();\n                                }\n                                \/\/ X\u00c3\u00b3a card back kh\u00e1\u00bb\u008fi gallery\n                                if (savedBackId && savedBackId !== savedFrontId) {\n                                    const backCard = document.querySelector('#pick-gallery-item .card[data-id=\"' + savedBackId + '\"]');\n                                    if (backCard) backCard.remove();\n                                }\n\n                                \/\/ Reset selections\n                                selectedFrontId = null;\n                                selectedBackId = null;\n                                cittBackgroundOcrJob = null;\n                                syncSubmitButtonVisibility();\n\n                                \/\/ Reset previews\n                                const previewBefore = document.getElementById('preview-before');\n                                if (previewBefore) previewBefore.innerHTML = '<div class=\"hint\">No front side selected<\/div>';\n                                const previewAfter = document.getElementById('preview-after');\n                                if (previewAfter) previewAfter.innerHTML = '<div class=\"hint\">No back side selected<\/div>';\n\n                                \/\/ Reset input files\n                                ['image-before', 'image-after'].forEach(inputId => {\n                                    const inputEl = document.getElementById(inputId);\n                                    if (inputEl) {\n                                        const dt = new DataTransfer();\n                                        inputEl.files = dt.files;\n                                    }\n                                });\n\n                                \/\/ \u00e1\u00ba\u00a8n result div, hi\u00e1\u00bb\u2021n l\u00e1\u00ba\u00a1i form\n                                const resultDiv2 = document.getElementById('citt-result');\n                                if (resultDiv2) {\n                                    resultDiv2.style.display = 'none';\n                                    resultDiv2.innerHTML = '';\n                                }\n\n                                \/\/ Re-enable submit button (gi\u00e1\u00bb\u00af icon)\n                                const uploadForm2 = document.getElementById('citt-upload-form');\n                                const submitBtn2 = uploadForm2?.querySelector('button[type=\"submit\"]');\n                                if (submitBtn2) {\n                                    submitBtn2.disabled = false;\n                                    submitBtn2.innerHTML = '<img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/upload.png\" alt=\"T&#x1EA3;i l&#xEA;n\"> T&#x1EA3;i L&#xEA;n &amp; Chuy&#x1EC3;n Text';\n                                    submitBtn2.style.display = '';\n                                }\n\n                                \/\/ Hi\u00e1\u00bb\u2021n form upload\n                                if (uploadForm2) uploadForm2.style.display = 'block';\n\n                                \/\/ X\u00c3\u00b3a badge front\/back tr\u00c3\u00aan t\u00e1\u00ba\u00a5t c\u00e1\u00ba\u00a3 cards c\u00c3\u00b2n l\u00e1\u00ba\u00a1i\n                                document.querySelectorAll('#pick-gallery-item .card .tag-badge').forEach(b => b.remove());\n\n                                \/\/ Check h\u00e1\u00ba\u00bft item \u00e2\u2020\u2019 redirect; n\u1ebfu c\u00f2n \u1ea3nh th\u00ec cu\u1ed9n v\u1ec1 danh s\u00e1ch \u0111\u1ec3 OCR ti\u1ebfp.\n                                const isEmptyAfterSave = checkAndRedirectIfEmpty();\n                                if (!isEmptyAfterSave) scrollToRemainingNamecards();\n                            }, 800);\n                        } else {\n                            saveStatus.innerHTML = '<span style=\"color:red;\">&#10060; ' + (sj.data?.message || 'L\u1ed7i khi l\u01b0u') + '<\/span>';\n                            btnLuu.disabled = false;\n                            btnLuu.textContent = '\ud83d\udcbe Save Name Card';\n                        }\n                    } catch(err) {\n                        saveStatus.innerHTML = '<span style=\"color:red;\">&#10060; L\u1ed7i k\u1ebft n\u1ed1i: ' + err.message + '<\/span>';\n                        btnLuu.disabled = false;\n                        btnLuu.textContent = '\ud83d\udcbe Save Name Card';\n                    }\n                });\n\n                \/\/ N\u00c3\u00bat H\u00e1\u00bb\u00a7y - ch\u00e1\u00bb\u2030 h\u00e1\u00bb\u00a7y k\u00e1\u00ba\u00bft qu\u00e1\u00ba\u00a3 OCR hi\u00e1\u00bb\u2021n t\u00e1\u00ba\u00a1i, KH\u00c3\u201dNG x\u00c3\u00b3a card trong waiting list\n                document.getElementById('btn-cancel-save').addEventListener('click', () => {\n                    \/\/ \u00e1\u00ba\u00a8n result div OCR\n                    const resultDiv3 = document.getElementById('citt-result');\n                    if (resultDiv3) { resultDiv3.style.display = 'none'; resultDiv3.innerHTML = ''; }\n\n                    \/\/ Gi\u00e1\u00bb\u00af nguy\u00c3\u00aan selections\/preview\/input \u00c4\u2018\u00e1\u00bb\u0192 user ch\u00e1\u00bb\u008dn b\u00e1\u00bb\u2022 sung m\u00e1\u00ba\u00b7t c\u00c3\u00b2n thi\u00e1\u00ba\u00bfu r\u00e1\u00bb\u201ci OCR l\u00e1\u00ba\u00a1i\n                    syncFlipPreview();\n                    syncSubmitButtonVisibility();\n\n                    \/\/ Re-enable submit button\n                    const uploadForm3 = document.getElementById('citt-upload-form');\n                    const submitBtn3 = uploadForm3?.querySelector('button[type=\"submit\"]');\n                    if (submitBtn3) {\n                        submitBtn3.disabled = false;\n                        submitBtn3.innerHTML = '<img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/upload.png\" alt=\"T&#x1EA3;i l&#xEA;n\"> T&#x1EA3;i L&#xEA;n &amp; Chuy&#x1EC3;n Text';\n                        submitBtn3.style.display = '';\n                    }\n                    if (uploadForm3) uploadForm3.style.display = 'block';\n                });\n\n            } else {\n                resultDiv.innerHTML = '<p style=\"color:red;\">\u00e2\u009d\u0152 ' + (json.data && json.data.message ? json.data.message : 'L\u00e1\u00bb\u2014i kh\u00c3\u00b4ng x\u00c3\u00a1c \u00c4\u2018\u00e1\u00bb\u2039nh') + '<\/p>';\n            }\n        } catch (err) {\n            stopConvertProgress(100);\n            resultDiv.innerHTML = '<p style=\"color:red;\">\u00e2\u009d\u0152 L\u00e1\u00bb\u2014i k\u00e1\u00ba\u00bft n\u00e1\u00bb\u2018i: ' + err.message + '<\/p>';\n        } finally {\n            clearInterval(convertProgressTimer);\n            if (submitBtn) {\n                submitBtn.disabled = false;\n                submitBtn.innerHTML = '<img decoding=\"async\" src=\"\/wp-content\/plugins\/convert-img-to-text\/assets\/image\/upload.png\" alt=\"T&#x1EA3;i l&#xEA;n\"> T&#x1EA3;i L&#xEA;n &amp; Chuy&#x1EC3;n Text';\n                submitBtn.style.display = '';\n            }\n        }\n    });\n\n    \/\/ Initialize step 1\n    setStep(1);\n\n    \/\/ Translate AUTO button text dynamically\n    document.addEventListener('DOMContentLoaded', function() {\n        const jpTranslations = {\n            'AUTO': '\u81ea\u52d5'\n        };\n\n        \/\/ Replace AUTO text in all cam-toggle-orient buttons\n        document.querySelectorAll('.cam-toggle-orient').forEach(btn => {\n            if (btn.textContent.includes('AUTO')) {\n                btn.textContent = jpTranslations['AUTO'] || btn.textContent;\n            }\n        });\n    });\n<\/script>\n\n\t\t\t<\/div>\n\t\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\n\t\n<\/div>\n\t\t<\/div>\n\n\t\t\n<style>\n#section_428637513 {\n  padding-top: 30px;\n  padding-bottom: 30px;\n  min-height: 51vh;\n}\n<\/style>\n\t<\/section>\n\t\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"page-blank.php","meta":{"footnotes":""},"class_list":["post-2","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/pages\/2","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/comments?post=2"}],"version-history":[{"count":45,"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/pages\/2\/revisions"}],"predecessor-version":[{"id":1560,"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/pages\/2\/revisions\/1560"}],"wp:attachment":[{"href":"https:\/\/namecard.kanetsu.com.vn\/en\/wp-json\/wp\/v2\/media?parent=2"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}