{"id":1504,"date":"2026-09-06T05:58:51","date_gmt":"2026-09-06T05:58:51","guid":{"rendered":"https:\/\/nscontinent.com\/?page_id=1504"},"modified":"2026-09-06T06:05:22","modified_gmt":"2026-09-06T06:05:22","slug":"3d-load-planner","status":"publish","type":"page","link":"https:\/\/nscontinent.com\/id\/3d-load-planner\/","title":{"rendered":"Perencana Muatan 3D"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"1504\" class=\"elementor elementor-1504\">\n\t\t\t\t<div class=\"elementor-element elementor-element-ba304fe e-con e-atomic-element e-flexbox-base e-default-div e-11d81c8 \" data-id=\"ba304fe\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"ba304fe\">\n    \t\t<div class=\"elementor-element elementor-element-76601c4 elementor-widget__width-inherit elementor-widget elementor-widget-html\" data-id=\"76601c4\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!-- =========================================================\r\n     NS CONTINENT 3D LOAD PLANNER\r\n     ELEMENTOR \/ BODY-ONLY DEPLOYMENT\r\n     SINGLE-INSTANCE DOM GUARD ENABLED\r\n========================================================= -->\r\n<style>\r\n\/* =========================================================\r\n   NS CONTINENT 3D LOAD PLANNER\r\n   V2.4.2 \/ USER-FRIENDLY WORKFLOW \/ EQUIPMENT-SPECIFIC RULES \/ FLAT-RACK CLEARANCE \/ GROUND SEPARATION \/ BODY-ONLY \/ ELEMENTOR FLEXBOX SAFE BASELINE\r\n\r\n   LOCKS\r\n   ---------------------------------------------------------\r\n   - NO WEBSITE HEADER\r\n   - NO WEBSITE FOOTER\r\n   - FULL WIDTH INSIDE FLEXBOX PARENT\r\n   - NO 100vw\r\n   - NO NEGATIVE BREAKOUT\r\n   - ROOT ISOLATED FROM WORDPRESS \/ ELEMENTOR STYLES\r\n   - CONTAINER \/ CARGO DATABASE IS CONFIGURABLE\r\n   - PACKING ENGINE IS HEURISTIC PLANNING, NOT AN ENGINEERING\r\n     OR CARRIER APPROVAL\r\n========================================================= *\/\r\n\r\n.nsc-load-planner {\r\n    --nsc-navy:#012d5a;\r\n    --nsc-navy-dark:#001b35;\r\n    --nsc-navy-deep:#001326;\r\n    --nsc-blue:#0b4f8a;\r\n    --nsc-blue-light:#eef5fb;\r\n    --nsc-gold:#c9a227;\r\n    --nsc-gold-light:#e3c65b;\r\n    --nsc-text:#172b3a;\r\n    --nsc-muted:#687989;\r\n    --nsc-white:#ffffff;\r\n    --nsc-light:#f5f8fb;\r\n    --nsc-border:#dce5ed;\r\n    --nsc-green:#16855b;\r\n    --nsc-green-light:#edf8f3;\r\n    --nsc-orange:#a66b12;\r\n    --nsc-orange-light:#fff7e8;\r\n    --nsc-red:#b53026;\r\n    --nsc-red-light:#fff0ee;\r\n    --nsc-frame:1400px;\r\n\r\n    display:block !important;\r\n    width:100% !important;\r\n    max-width:100% !important;\r\n    min-width:0 !important;\r\n    min-inline-size:0 !important;\r\n    margin:0 !important;\r\n    padding:0 !important;\r\n    overflow-x:hidden !important;\r\n    overflow-y:visible;\r\n    position:relative;\r\n    box-sizing:border-box;\r\n    color:var(--nsc-text);\r\n    background:var(--nsc-light);\r\n    font-family:Arial,Helvetica,sans-serif;\r\n    line-height:1.55;\r\n}\r\n\r\n.nsc-load-planner *,\r\n.nsc-load-planner *::before,\r\n.nsc-load-planner *::after { box-sizing:border-box; }\r\n\r\n.nsc-load-planner button,\r\n.nsc-load-planner input,\r\n.nsc-load-planner select { font:inherit; }\r\n\r\n.nsc-load-planner button { appearance:none; -webkit-appearance:none; }\r\n\r\n.nsc-load-planner .nsc-lp-shell {\r\n    width:100%;\r\n    max-width:100%;\r\n    min-width:0;\r\n    margin:0;\r\n    padding:0;\r\n}\r\n\r\n.nsc-lp-inner {\r\n    width:min(var(--nsc-frame),calc(100% - 60px));\r\n    max-width:var(--nsc-frame);\r\n    min-width:0;\r\n    margin:0 auto;\r\n}\r\n\r\n\/* =========================================================\r\n   HERO\r\n========================================================= *\/\r\n.nsc-lp-hero {\r\n    width:100%;\r\n    min-width:0;\r\n    position:relative;\r\n    overflow:hidden;\r\n    color:#fff;\r\n    background:linear-gradient(135deg,#001326,#012d5a,#0a4b84);\r\n}\r\n\r\n.nsc-lp-hero::before {\r\n    content:\"\";\r\n    position:absolute;\r\n    width:620px;\r\n    height:620px;\r\n    right:-300px;\r\n    top:-340px;\r\n    border:1px solid rgba(255,255,255,.08);\r\n    border-radius:50%;\r\n}\r\n\r\n.nsc-lp-hero-inner {\r\n    position:relative;\r\n    z-index:2;\r\n    padding:64px 0 72px;\r\n}\r\n\r\n.nsc-lp-eyebrow {\r\n    display:inline-flex;\r\n    align-items:center;\r\n    gap:8px;\r\n    color:#e8d7b2;\r\n    font-size:10px;\r\n    font-weight:900;\r\n    letter-spacing:2px;\r\n    text-transform:uppercase;\r\n}\r\n\r\n.nsc-lp-eyebrow-dot {\r\n    width:6px;\r\n    height:6px;\r\n    flex:0 0 6px;\r\n    border-radius:50%;\r\n    background:var(--nsc-gold);\r\n}\r\n\r\n.nsc-lp-hero h1 {\r\n    max-width:900px;\r\n    margin:14px 0 0;\r\n    color:#fff !important;\r\n    font-family:Arial,Helvetica,sans-serif !important;\r\n    font-size:clamp(38px,5vw,66px);\r\n    font-weight:900 !important;\r\n    line-height:1.02;\r\n    letter-spacing:-2px;\r\n}\r\n\r\n.nsc-lp-hero h1 span { color:#e8d7b2; }\r\n\r\n.nsc-lp-hero-copy {\r\n    max-width:880px;\r\n    margin:18px 0 0;\r\n    color:rgba(255,255,255,.76);\r\n    font-size:14px;\r\n    line-height:1.8;\r\n}\r\n\r\n.nsc-lp-hero-pills {\r\n    display:flex;\r\n    flex-wrap:wrap;\r\n    gap:8px;\r\n    margin-top:24px;\r\n}\r\n\r\n.nsc-lp-hero-pill {\r\n    padding:7px 10px;\r\n    border:1px solid rgba(255,255,255,.13);\r\n    background:rgba(255,255,255,.06);\r\n    color:rgba(255,255,255,.82);\r\n    font-size:9px;\r\n    font-weight:800;\r\n    letter-spacing:.5px;\r\n}\r\n\r\n\/* =========================================================\r\n   CONTROL \/ WORKSPACE\r\n========================================================= *\/\r\n.nsc-lp-workspace {\r\n    width:100%;\r\n    min-width:0;\r\n    background:var(--nsc-light);\r\n}\r\n\r\n.nsc-lp-workspace-inner {\r\n    padding:28px 0 70px;\r\n}\r\n\r\n.nsc-lp-toolbar {\r\n    display:grid;\r\n    grid-template-columns:minmax(0,1.2fr) minmax(0,.8fr);\r\n    gap:16px;\r\n    min-width:0;\r\n}\r\n\r\n.nsc-lp-card {\r\n    min-width:0;\r\n    background:#fff;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:14px;\r\n    box-shadow:0 10px 30px rgba(1,45,90,.055);\r\n}\r\n\r\n.nsc-lp-controls {\r\n    padding:20px;\r\n}\r\n\r\n.nsc-lp-card-title {\r\n    display:flex;\r\n    align-items:center;\r\n    gap:8px;\r\n    margin:0 0 16px;\r\n    color:var(--nsc-navy);\r\n    font-size:14px;\r\n    font-weight:900;\r\n}\r\n\r\n.nsc-lp-card-title::before {\r\n    content:\"\";\r\n    width:5px;\r\n    height:20px;\r\n    flex:0 0 5px;\r\n    border-radius:4px;\r\n    background:var(--nsc-gold);\r\n}\r\n\r\n.nsc-lp-form-grid {\r\n    display:grid;\r\n    grid-template-columns:repeat(2,minmax(0,1fr));\r\n    gap:12px;\r\n}\r\n\r\n.nsc-lp-field { min-width:0; }\r\n\r\n.nsc-lp-field label {\r\n    display:block;\r\n    margin-bottom:6px;\r\n    color:var(--nsc-navy);\r\n    font-size:10px;\r\n    font-weight:900;\r\n    letter-spacing:.7px;\r\n    text-transform:uppercase;\r\n}\r\n\r\n.nsc-lp-input,\r\n.nsc-lp-select {\r\n    display:block;\r\n    width:100%;\r\n    min-width:0;\r\n    height:46px;\r\n    padding:0 12px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:8px;\r\n    outline:none;\r\n    background:#fff;\r\n    color:var(--nsc-text);\r\n    font-size:12px;\r\n}\r\n\r\n.nsc-lp-input:focus,\r\n.nsc-lp-select:focus {\r\n    border-color:#6e9bc0;\r\n    box-shadow:0 0 0 3px rgba(11,79,138,.09);\r\n}\r\n\r\n.nsc-lp-help {\r\n    margin-top:5px;\r\n    color:#8795a3;\r\n    font-size:9px;\r\n    line-height:1.45;\r\n}\r\n\r\n.nsc-lp-actions {\r\n    display:flex;\r\n    flex-wrap:wrap;\r\n    gap:8px;\r\n    margin-top:15px;\r\n}\r\n\r\n.nsc-lp-button {\r\n    min-height:42px;\r\n    padding:0 15px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:9px;\r\n    background:#fff;\r\n    color:var(--nsc-navy);\r\n    font-size:10px;\r\n    font-weight:900;\r\n    letter-spacing:.5px;\r\n    cursor:pointer;\r\n    transition:transform .16s ease,box-shadow .16s ease,filter .16s ease;\r\n}\r\n\r\n.nsc-lp-button:hover {\r\n    transform:translateY(-1px);\r\n    box-shadow:0 8px 18px rgba(1,45,90,.08);\r\n}\r\n\r\n.nsc-lp-button-primary {\r\n    color:#fff;\r\n    border-color:transparent;\r\n    background:linear-gradient(135deg,#012d5a,#0b558f);\r\n}\r\n\r\n.nsc-lp-button-danger {\r\n    color:#fff;\r\n    border-color:transparent;\r\n    background:#b53026;\r\n}\r\n\r\n.nsc-lp-button-green {\r\n    color:#fff;\r\n    border-color:transparent;\r\n    background:var(--nsc-green);\r\n}\r\n\r\n.nsc-lp-status-card {\r\n    padding:20px;\r\n}\r\n\r\n.nsc-lp-kpi-grid {\r\n    display:grid;\r\n    grid-template-columns:repeat(2,minmax(0,1fr));\r\n    gap:10px;\r\n}\r\n\r\n.nsc-lp-kpi {\r\n    min-width:0;\r\n    padding:14px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:10px;\r\n    background:#fbfdff;\r\n}\r\n\r\n.nsc-lp-kpi-label {\r\n    color:var(--nsc-muted);\r\n    font-size:8px;\r\n    font-weight:900;\r\n    letter-spacing:.8px;\r\n    text-transform:uppercase;\r\n}\r\n\r\n.nsc-lp-kpi-value {\r\n    margin-top:4px;\r\n    color:var(--nsc-navy);\r\n    font-size:20px;\r\n    font-weight:900;\r\n    line-height:1.15;\r\n    overflow-wrap:anywhere;\r\n}\r\n\r\n.nsc-lp-kpi-small {\r\n    margin-top:2px;\r\n    color:var(--nsc-muted);\r\n    font-size:9px;\r\n}\r\n\r\n.nsc-lp-alert {\r\n    margin-top:12px;\r\n    padding:11px 12px;\r\n    border:1px solid #eadbb9;\r\n    border-left:4px solid var(--nsc-gold);\r\n    border-radius:9px;\r\n    background:#fffaf0;\r\n    color:#6e6046;\r\n    font-size:10px;\r\n    line-height:1.55;\r\n}\r\n\r\n.nsc-lp-alert.error {\r\n    border-color:#f0c9c5;\r\n    border-left-color:var(--nsc-red);\r\n    background:var(--nsc-red-light);\r\n    color:#8d3028;\r\n}\r\n\r\n.nsc-lp-alert.success {\r\n    border-color:#d6eee2;\r\n    border-left-color:var(--nsc-green);\r\n    background:var(--nsc-green-light);\r\n    color:#346d56;\r\n}\r\n\r\n\/* =========================================================\r\n   STEP 2 ACTION BAR\r\n   ACTIONS STAY AT THE END OF CARGO INPUT\r\n========================================================= *\/\r\n.nsc-lp-cargo-actions {\r\n    display:flex;\r\n    align-items:center;\r\n    justify-content:space-between;\r\n    gap:14px;\r\n    margin-top:16px;\r\n    padding:15px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:11px;\r\n    background:linear-gradient(180deg,#fbfdff,#f7fafc);\r\n}\r\n\r\n.nsc-lp-cargo-actions-copy {\r\n    min-width:0;\r\n    color:var(--nsc-muted);\r\n    font-size:10px;\r\n    line-height:1.55;\r\n}\r\n\r\n.nsc-lp-cargo-actions-copy strong {\r\n    color:var(--nsc-navy);\r\n}\r\n\r\n.nsc-lp-cargo-actions .nsc-lp-actions {\r\n    flex:0 0 auto;\r\n    justify-content:flex-end;\r\n    margin-top:0;\r\n}\r\n\r\n.nsc-lp-cargo-actions .nsc-lp-button-primary {\r\n    min-width:178px;\r\n}\r\n\r\n\/* =========================================================\r\n   CARGO TABLE\r\n========================================================= *\/\r\n.nsc-lp-cargo-card {\r\n    margin-top:16px;\r\n    padding:20px;\r\n}\r\n\r\n.nsc-lp-cargo-head {\r\n    display:flex;\r\n    align-items:flex-end;\r\n    justify-content:space-between;\r\n    gap:15px;\r\n    margin-bottom:15px;\r\n}\r\n\r\n.nsc-lp-cargo-head p {\r\n    margin:4px 0 0;\r\n    color:var(--nsc-muted);\r\n    font-size:10px;\r\n}\r\n\r\n.nsc-lp-table-wrap {\r\n    width:100%;\r\n    max-width:100%;\r\n    overflow-x:auto;\r\n    -webkit-overflow-scrolling:touch;\r\n}\r\n\r\n.nsc-lp-cargo-table {\r\n    width:100%;\r\n    min-width:1380px;\r\n    border-collapse:collapse;\r\n}\r\n\r\n.nsc-lp-cargo-table th {\r\n    padding:9px 7px;\r\n    color:var(--nsc-muted);\r\n    background:#f7f9fc;\r\n    border-bottom:1px solid var(--nsc-border);\r\n    text-align:left;\r\n    font-size:8px;\r\n    font-weight:900;\r\n    letter-spacing:.6px;\r\n    text-transform:uppercase;\r\n}\r\n\r\n.nsc-lp-cargo-table td {\r\n    padding:7px;\r\n    border-bottom:1px solid #edf1f5;\r\n    vertical-align:middle;\r\n    font-size:10px;\r\n}\r\n\r\n.nsc-lp-cargo-table tbody tr:last-child td { border-bottom:none; }\r\n\r\n.nsc-lp-mini-input,\r\n.nsc-lp-mini-select {\r\n    width:100%;\r\n    height:36px;\r\n    padding:0 7px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:7px;\r\n    background:#fff;\r\n    color:var(--nsc-text);\r\n    outline:none;\r\n    font-size:10px;\r\n}\r\n\r\n.nsc-lp-mini-input:focus,\r\n.nsc-lp-mini-select:focus {\r\n    border-color:#6e9bc0;\r\n    box-shadow:0 0 0 2px rgba(11,79,138,.08);\r\n}\r\n\r\n.nsc-lp-color {\r\n    width:28px;\r\n    height:28px;\r\n    padding:0;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:7px;\r\n    background:#fff;\r\n}\r\n\r\n.nsc-lp-row-remove {\r\n    width:32px;\r\n    height:32px;\r\n    border:1px solid #ead0cc;\r\n    border-radius:7px;\r\n    background:#fff;\r\n    color:var(--nsc-red);\r\n    font-weight:900;\r\n    cursor:pointer;\r\n}\r\n\r\n\/* =========================================================\r\n   CARGO HANDLING CONTROLS\r\n========================================================= *\/\r\n.nsc-lp-handling-badge {\r\n    display:inline-flex;\r\n    align-items:center;\r\n    justify-content:center;\r\n    min-height:26px;\r\n    padding:0 8px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:7px;\r\n    background:#fff;\r\n    color:var(--nsc-navy);\r\n    font-size:8px;\r\n    font-weight:900;\r\n    white-space:nowrap;\r\n}\r\n\r\n.nsc-lp-handling-note {\r\n    margin-top:8px;\r\n    padding:9px 11px;\r\n    border:1px solid #d8e8f5;\r\n    border-radius:8px;\r\n    background:var(--nsc-blue-light);\r\n    color:var(--nsc-muted);\r\n    font-size:9px;\r\n    line-height:1.55;\r\n}\r\n\r\n.nsc-lp-orientation-strip {\r\n    display:grid;\r\n    grid-template-columns:1fr 1fr;\r\n    gap:8px;\r\n    padding:9px 12px;\r\n    border-bottom:1px solid var(--nsc-border);\r\n    background:#f8fafc;\r\n}\r\n\r\n.nsc-lp-orientation-side {\r\n    display:flex;\r\n    align-items:center;\r\n    gap:8px;\r\n    min-width:0;\r\n    padding:7px 9px;\r\n    border-radius:8px;\r\n    font-size:8px;\r\n    font-weight:900;\r\n    letter-spacing:.5px;\r\n    text-transform:uppercase;\r\n}\r\n\r\n.nsc-lp-orientation-inside {\r\n    color:var(--nsc-navy);\r\n    background:rgba(11,79,138,.08);\r\n    border:1px solid rgba(11,79,138,.16);\r\n}\r\n\r\n.nsc-lp-orientation-door {\r\n    justify-content:flex-end;\r\n    color:#8a5706;\r\n    background:rgba(245,158,11,.10);\r\n    border:1px solid rgba(245,158,11,.23);\r\n}\r\n\r\n.nsc-lp-orientation-dot {\r\n    width:9px;\r\n    height:9px;\r\n    flex:0 0 9px;\r\n    border-radius:50%;\r\n}\r\n\r\n.nsc-lp-orientation-inside .nsc-lp-orientation-dot {\r\n    background:#0b4f8a;\r\n}\r\n\r\n.nsc-lp-orientation-door .nsc-lp-orientation-dot {\r\n    background:#f59e0b;\r\n}\r\n\r\n.nsc-lp-cargo-guidance {\r\n    margin-top:10px;\r\n    padding:9px 11px;\r\n    border-left:3px solid var(--nsc-gold);\r\n    border-radius:7px;\r\n    background:#fffaf0;\r\n    color:#6e6046;\r\n    font-size:9px;\r\n    line-height:1.55;\r\n}\r\n\r\n\/* =========================================================\r\n   3D VIEW + REPORT\r\n========================================================= *\/\r\n.nsc-lp-view-grid {\r\n    display:grid;\r\n    grid-template-columns:minmax(0,1.8fr) minmax(290px,.7fr);\r\n    gap:16px;\r\n    margin-top:16px;\r\n    min-width:0;\r\n}\r\n\r\n.nsc-lp-view-card {\r\n    min-width:0;\r\n    overflow:hidden;\r\n}\r\n\r\n.nsc-lp-view-toolbar {\r\n    display:flex;\r\n    align-items:center;\r\n    justify-content:space-between;\r\n    gap:10px;\r\n    padding:12px 15px;\r\n    border-bottom:1px solid var(--nsc-border);\r\n    background:#fff;\r\n}\r\n\r\n.nsc-lp-view-title {\r\n    color:var(--nsc-navy);\r\n    font-size:11px;\r\n    font-weight:900;\r\n}\r\n\r\n.nsc-lp-view-sub {\r\n    color:var(--nsc-muted);\r\n    font-size:9px;\r\n}\r\n\r\n.nsc-lp-view-actions {\r\n    display:flex;\r\n    align-items:center;\r\n    justify-content:flex-end;\r\n    flex-wrap:wrap;\r\n    gap:6px;\r\n    flex:0 0 auto;\r\n}\r\n\r\n.nsc-lp-view-button {\r\n    min-height:32px;\r\n    padding:0 10px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:7px;\r\n    background:#ffffff;\r\n    color:var(--nsc-navy);\r\n    font-size:9px;\r\n    font-weight:900;\r\n    letter-spacing:.3px;\r\n    cursor:pointer;\r\n    transition:background .16s ease, color .16s ease, border-color .16s ease, transform .16s ease, box-shadow .16s ease;\r\n}\r\n\r\n.nsc-lp-view-button:hover {\r\n    transform:translateY(-1px);\r\n    background:#f8fafc;\r\n    border-color:#bfd2e3;\r\n    box-shadow:0 6px 14px rgba(1,45,90,.07);\r\n}\r\n\r\n.nsc-lp-view-button.active {\r\n    background:var(--nsc-navy);\r\n    border-color:var(--nsc-navy);\r\n    color:#ffffff;\r\n}\r\n\r\n.nsc-lp-canvas-wrap {\r\n    position:relative;\r\n    width:100%;\r\n    min-width:0;\r\n    height:620px;\r\n    background:linear-gradient(180deg,#eef4f8,#dfe8ef);\r\n}\r\n\r\n#nscLpCanvas {\r\n    display:block;\r\n    width:100% !important;\r\n    height:100% !important;\r\n}\r\n\r\n.nsc-lp-canvas-note {\r\n    position:absolute;\r\n    left:12px;\r\n    bottom:12px;\r\n    max-width:320px;\r\n    padding:8px 10px;\r\n    border:1px solid rgba(1,45,90,.12);\r\n    border-radius:8px;\r\n    background:rgba(255,255,255,.88);\r\n    color:#667789;\r\n    font-size:9px;\r\n    line-height:1.45;\r\n    backdrop-filter:blur(7px);\r\n}\r\n\r\n.nsc-lp-report {\r\n    min-width:0;\r\n    padding:16px;\r\n}\r\n\r\n.nsc-lp-report-section + .nsc-lp-report-section {\r\n    margin-top:16px;\r\n    padding-top:16px;\r\n    border-top:1px solid var(--nsc-border);\r\n}\r\n\r\n.nsc-lp-report-row {\r\n    display:flex;\r\n    justify-content:space-between;\r\n    align-items:flex-start;\r\n    gap:12px;\r\n    padding:8px 0;\r\n    border-bottom:1px solid #edf1f5;\r\n    font-size:10px;\r\n}\r\n\r\n.nsc-lp-report-row:last-child { border-bottom:none; }\r\n\r\n.nsc-lp-report-label { color:var(--nsc-muted); }\r\n\r\n.nsc-lp-report-value {\r\n    color:var(--nsc-navy);\r\n    font-weight:900;\r\n    text-align:right;\r\n}\r\n\r\n.nsc-lp-fit-badge {\r\n    display:inline-flex;\r\n    align-items:center;\r\n    justify-content:center;\r\n    min-width:78px;\r\n    padding:6px 9px;\r\n    border-radius:999px;\r\n    background:var(--nsc-green-light);\r\n    color:var(--nsc-green);\r\n    font-size:9px;\r\n    font-weight:900;\r\n}\r\n\r\n.nsc-lp-fit-badge.warning {\r\n    background:var(--nsc-orange-light);\r\n    color:var(--nsc-orange);\r\n}\r\n\r\n.nsc-lp-fit-badge.error {\r\n    background:var(--nsc-red-light);\r\n    color:var(--nsc-red);\r\n}\r\n\r\n.nsc-lp-shipper-list {\r\n    display:grid;\r\n    gap:7px;\r\n}\r\n\r\n.nsc-lp-shipper-row {\r\n    display:flex;\r\n    align-items:center;\r\n    justify-content:space-between;\r\n    gap:8px;\r\n    padding:7px 0;\r\n    border-bottom:1px solid #edf1f5;\r\n    font-size:9px;\r\n}\r\n\r\n.nsc-lp-shipper-row:last-child { border-bottom:none; }\r\n\r\n.nsc-lp-shipper-name {\r\n    display:flex;\r\n    align-items:center;\r\n    gap:7px;\r\n    min-width:0;\r\n}\r\n\r\n.nsc-lp-shipper-dot {\r\n    width:9px;\r\n    height:9px;\r\n    flex:0 0 9px;\r\n    border-radius:50%;\r\n}\r\n\r\n.nsc-lp-shipper-name span:last-child {\r\n    overflow-wrap:anywhere;\r\n}\r\n\r\n.nsc-lp-unplaced {\r\n    margin-top:8px;\r\n    padding:10px;\r\n    border:1px solid #f0c9c5;\r\n    border-radius:8px;\r\n    background:var(--nsc-red-light);\r\n    color:#8d3028;\r\n    font-size:9px;\r\n    line-height:1.55;\r\n}\r\n\r\n\/* =========================================================\r\n   DATABASE NOTE\r\n========================================================= *\/\r\n.nsc-lp-database-card {\r\n    margin-top:16px;\r\n    padding:20px;\r\n}\r\n\r\n.nsc-lp-database-grid {\r\n    display:grid;\r\n    grid-template-columns:repeat(3,minmax(0,1fr));\r\n    gap:10px;\r\n}\r\n\r\n.nsc-lp-db-item {\r\n    padding:12px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:9px;\r\n    background:#fbfdff;\r\n}\r\n\r\n.nsc-lp-db-item strong {\r\n    display:block;\r\n    color:var(--nsc-navy);\r\n    font-size:10px;\r\n}\r\n\r\n.nsc-lp-db-item span {\r\n    display:block;\r\n    margin-top:3px;\r\n    color:var(--nsc-muted);\r\n    font-size:9px;\r\n    line-height:1.45;\r\n}\r\n\r\n.nsc-lp-disclaimer {\r\n    margin-top:12px;\r\n    padding:10px 12px;\r\n    border:1px solid #eadbb9;\r\n    border-left:4px solid var(--nsc-gold);\r\n    border-radius:8px;\r\n    background:#fffaf0;\r\n    color:#6e6046;\r\n    font-size:9px;\r\n    line-height:1.55;\r\n}\r\n\r\n\r\n\/* =========================================================\r\n   USER-FRIENDLY WORKFLOW\r\n========================================================= *\/\r\n.nsc-lp-stepbar{\r\n    display:grid;\r\n    grid-template-columns:repeat(3,minmax(0,1fr));\r\n    gap:10px;\r\n    margin-bottom:16px;\r\n}\r\n.nsc-lp-step{\r\n    display:flex;\r\n    align-items:flex-start;\r\n    gap:10px;\r\n    min-width:0;\r\n    padding:13px 14px;\r\n    border:1px solid var(--nsc-border);\r\n    border-radius:11px;\r\n    background:#fff;\r\n}\r\n.nsc-lp-step-number{\r\n    width:28px;\r\n    height:28px;\r\n    flex:0 0 28px;\r\n    display:flex;\r\n    align-items:center;\r\n    justify-content:center;\r\n    border-radius:50%;\r\n    background:var(--nsc-blue-light);\r\n    color:var(--nsc-navy);\r\n    font-size:10px;\r\n    font-weight:900;\r\n}\r\n.nsc-lp-step-title{\r\n    color:var(--nsc-navy);\r\n    font-size:10px;\r\n    font-weight:900;\r\n    line-height:1.3;\r\n}\r\n.nsc-lp-step-copy{\r\n    margin-top:2px;\r\n    color:var(--nsc-muted);\r\n    font-size:9px;\r\n    line-height:1.45;\r\n}\r\n.nsc-lp-section-intro{\r\n    margin:-7px 0 14px;\r\n    color:var(--nsc-muted);\r\n    font-size:10px;\r\n    line-height:1.6;\r\n}\r\n.nsc-lp-step-tag{\r\n    display:inline-flex;\r\n    align-items:center;\r\n    justify-content:center;\r\n    min-width:52px;\r\n    height:22px;\r\n    padding:0 7px;\r\n    border-radius:999px;\r\n    background:var(--nsc-navy);\r\n    color:#fff;\r\n    font-size:8px;\r\n    font-weight:900;\r\n    letter-spacing:.5px;\r\n    text-transform:uppercase;\r\n}\r\n.nsc-lp-action-guide{\r\n    display:block;\r\n    margin:12px 0 0;\r\n    padding:9px 11px;\r\n    border-radius:8px;\r\n    background:var(--nsc-blue-light);\r\n    color:var(--nsc-navy);\r\n    font-size:9px;\r\n    line-height:1.55;\r\n}\r\n.nsc-lp-action-guide strong{font-weight:900;}\r\n.nsc-lp-friendly-note{\r\n    margin:0 0 14px;\r\n    padding:10px 12px;\r\n    border:1px solid #d8e8f5;\r\n    border-radius:9px;\r\n    background:#f8fbfe;\r\n    color:#51677b;\r\n    font-size:9px;\r\n    line-height:1.6;\r\n}\r\n.nsc-lp-friendly-note strong{color:var(--nsc-navy);}\r\n.nsc-lp-advanced{\r\n    margin-top:13px;\r\n    border-top:1px solid var(--nsc-border);\r\n    padding-top:12px;\r\n}\r\n.nsc-lp-advanced summary{\r\n    cursor:pointer;\r\n    color:var(--nsc-navy);\r\n    font-size:10px;\r\n    font-weight:900;\r\n    list-style:none;\r\n}\r\n.nsc-lp-advanced summary::-webkit-details-marker{display:none;}\r\n.nsc-lp-advanced summary::before{\r\n    content:\"+\";\r\n    display:inline-flex;\r\n    align-items:center;\r\n    justify-content:center;\r\n    width:18px;\r\n    height:18px;\r\n    margin-right:6px;\r\n    border-radius:5px;\r\n    background:var(--nsc-blue-light);\r\n    color:var(--nsc-navy);\r\n}\r\n.nsc-lp-advanced[open] summary::before{content:\"\u2212\";}\r\n.nsc-lp-advanced .nsc-lp-form-grid{margin-top:12px;}\r\n.nsc-lp-cargo-step-head{\r\n    display:flex;\r\n    align-items:flex-start;\r\n    gap:10px;\r\n    margin-bottom:14px;\r\n}\r\n.nsc-lp-cargo-step-copy{min-width:0;}\r\n.nsc-lp-cargo-step-copy .nsc-lp-card-title{margin-bottom:3px;}\r\n.nsc-lp-cargo-step-copy p{margin:0;color:var(--nsc-muted);font-size:10px;line-height:1.6;}\r\n.nsc-lp-cargo-tip{\r\n    margin-top:9px;\r\n    padding:8px 10px;\r\n    border-radius:8px;\r\n    background:#fbfcfe;\r\n    color:#66788a;\r\n    font-size:9px;\r\n    line-height:1.5;\r\n}\r\n.nsc-lp-view-sub{max-width:430px;}\r\n.nsc-lp-view-guide{\r\n    display:flex;\r\n    flex-wrap:wrap;\r\n    gap:6px;\r\n    margin-top:7px;\r\n}\r\n.nsc-lp-view-guide span{\r\n    display:inline-flex;\r\n    align-items:center;\r\n    padding:5px 7px;\r\n    border-radius:6px;\r\n    background:#f7f9fc;\r\n    color:var(--nsc-muted);\r\n    font-size:8px;\r\n}\r\n\r\n\r\n.nsc-lp-visual-legend{\r\n    display:flex;\r\n    flex-wrap:wrap;\r\n    gap:6px;\r\n    margin-top:7px;\r\n}\r\n.nsc-lp-visual-legend span{\r\n    display:inline-flex;\r\n    align-items:center;\r\n    gap:5px;\r\n    padding:5px 7px;\r\n    border-radius:6px;\r\n    background:#f7f9fc;\r\n    color:var(--nsc-muted);\r\n    font-size:8px;\r\n}\r\n.nsc-lp-legend-dot{\r\n    width:7px;\r\n    height:7px;\r\n    flex:0 0 7px;\r\n    border-radius:50%;\r\n}\r\n.nsc-lp-legend-ground{background:#69c6ee;}\r\n.nsc-lp-legend-cargo{background:#0b4f8a;}\r\n.nsc-lp-legend-frame{background:#536273;}\r\n\r\n\r\n\/* =========================================================\r\n   EQUIPMENT-SPECIFIC RULES\r\n========================================================= *\/\r\n.nsc-lp-equipment-rule-box{\r\n    margin-top:12px;\r\n    padding:10px 12px;\r\n    border:1px solid #d8e8f5;\r\n    border-left:4px solid var(--nsc-gold);\r\n    border-radius:9px;\r\n    background:#fbfdff;\r\n    color:#53697c;\r\n    font-size:9px;\r\n    line-height:1.6;\r\n}\r\n.nsc-lp-equipment-rule-box strong{color:var(--nsc-navy);}\r\n\r\n.nsc-lp-gauge-badge{\r\n    display:inline-flex;\r\n    align-items:center;\r\n    justify-content:center;\r\n    min-width:66px;\r\n    padding:5px 7px;\r\n    border-radius:999px;\r\n    font-size:8px;\r\n    font-weight:900;\r\n    letter-spacing:.25px;\r\n    white-space:nowrap;\r\n}\r\n.nsc-lp-gauge-in{\r\n    background:var(--nsc-green-light);\r\n    color:var(--nsc-green);\r\n}\r\n.nsc-lp-gauge-out{\r\n    background:var(--nsc-orange-light);\r\n    color:var(--nsc-orange);\r\n}\r\n.nsc-lp-gauge-blocked{\r\n    background:var(--nsc-red-light);\r\n    color:var(--nsc-red);\r\n}\r\n.nsc-lp-special-rule{\r\n    margin-top:8px;\r\n    padding:8px 10px;\r\n    border-radius:8px;\r\n    background:#f7f9fc;\r\n    color:var(--nsc-muted);\r\n    font-size:8px;\r\n    line-height:1.55;\r\n}\r\n.nsc-lp-special-rule strong{color:var(--nsc-navy);}\r\n\r\n@media (max-width:800px){\r\n    .nsc-lp-equipment-rule-box{font-size:8.5px;}\r\n}\r\n\r\n\/* =========================================================\r\n   RESPONSIVE\r\n========================================================= *\/\r\n@media (max-width:1180px) {\r\n    .nsc-lp-inner {\r\n        width:calc(100% - 40px);\r\n        max-width:none;\r\n    }\r\n\r\n    .nsc-lp-toolbar {\r\n        grid-template-columns:1fr;\r\n    }\r\n\r\n    .nsc-lp-view-grid {\r\n        grid-template-columns:1fr;\r\n    }\r\n}\r\n\r\n@media (max-width:800px) {\r\n\r\n    .nsc-lp-cargo-actions {\r\n        align-items:stretch;\r\n        flex-direction:column;\r\n    }\r\n\r\n    .nsc-lp-cargo-actions .nsc-lp-actions {\r\n        width:100%;\r\n        flex-direction:column;\r\n    }\r\n\r\n    .nsc-lp-cargo-actions .nsc-lp-button,\r\n    .nsc-lp-cargo-actions .nsc-lp-button-primary {\r\n        width:100%;\r\n        min-width:0;\r\n    }\r\n\r\n    .nsc-lp-stepbar{grid-template-columns:1fr;}\r\n\r\n    .nsc-lp-view-toolbar {\r\n        align-items:flex-start;\r\n        flex-direction:column;\r\n    }\r\n\r\n    .nsc-lp-view-actions {\r\n        width:100%;\r\n        justify-content:flex-start;\r\n    }\r\n\r\n    .nsc-lp-hero-inner {\r\n        padding:52px 0 60px;\r\n    }\r\n\r\n    .nsc-lp-form-grid {\r\n        grid-template-columns:1fr;\r\n    }\r\n\r\n    .nsc-lp-kpi-grid {\r\n        grid-template-columns:1fr 1fr;\r\n    }\r\n\r\n    .nsc-lp-database-grid {\r\n        grid-template-columns:1fr 1fr;\r\n    }\r\n\r\n    .nsc-lp-canvas-wrap {\r\n        height:520px;\r\n    }\r\n}\r\n\r\n@media (max-width:560px) {\r\n    .nsc-lp-inner {\r\n        width:calc(100% - 24px);\r\n    }\r\n\r\n    .nsc-lp-hero h1 {\r\n        font-size:clamp(34px,10vw,48px);\r\n        letter-spacing:-1.2px;\r\n    }\r\n\r\n    .nsc-lp-hero-copy {\r\n        font-size:12px;\r\n    }\r\n\r\n    .nsc-lp-cargo-card,\r\n    .nsc-lp-controls,\r\n    .nsc-lp-status-card,\r\n    .nsc-lp-report,\r\n    .nsc-lp-database-card {\r\n        padding:16px;\r\n    }\r\n\r\n    .nsc-lp-canvas-wrap {\r\n        height:430px;\r\n    }\r\n\r\n    .nsc-lp-database-grid {\r\n        grid-template-columns:1fr;\r\n    }\r\n}\r\n<\/style>\r\n<div\r\n    id=\"nsc-lp-root\"\r\n    class=\"nsc-load-planner\"\r\n    data-nsc-load-planner-root=\"true\"\r\n>\r\n\r\n<div class=\"nsc-lp-shell\">\r\n\r\n<!-- =====================================================\r\n     HERO\r\n====================================================== -->\r\n<section class=\"nsc-lp-hero\">\r\n<div class=\"nsc-lp-inner\">\r\n<div class=\"nsc-lp-hero-inner\">\r\n\r\n<div class=\"nsc-lp-eyebrow\">\r\n<span class=\"nsc-lp-eyebrow-dot\"><\/span>\r\nNS CONTINENT 3D LOGISTICS TOOL\r\n<\/div>\r\n\r\n<h1>\r\n3D <span>Load Planner<\/span>\r\n<\/h1>\r\n\r\n<div class=\"nsc-lp-hero-copy\">\r\nSee how cargo from one or more shippers could fit inside a container.\r\nChoose the container, enter your cargo details in <strong>cm<\/strong>,\r\nthen create a 3D loading plan to see what fits and where it goes.\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-hero-pills\">\r\n<div class=\"nsc-lp-hero-pill\">MULTI-SHIPPER<\/div>\r\n<div class=\"nsc-lp-hero-pill\">CM INPUT<\/div>\r\n<div class=\"nsc-lp-hero-pill\">3D VIEW<\/div>\r\n<div class=\"nsc-lp-hero-pill\">WEIGHT AWARE<\/div>\r\n<div class=\"nsc-lp-hero-pill\">20 \/ 40 \/ 40HC<\/div>\r\n<div class=\"nsc-lp-hero-pill\">OPEN TOP \/ FLAT RACK<\/div>\r\n<\/div>\r\n\r\n<\/div>\r\n<\/div>\r\n<\/section>\r\n\r\n<!-- =====================================================\r\n     WORKSPACE\r\n====================================================== -->\r\n<section class=\"nsc-lp-workspace\">\r\n<div class=\"nsc-lp-inner\">\r\n<div class=\"nsc-lp-workspace-inner\">\r\n\r\n<div class=\"nsc-lp-stepbar\" aria-label=\"How to use the planner\">\r\n<div class=\"nsc-lp-step\"><div class=\"nsc-lp-step-number\">1<\/div><div><div class=\"nsc-lp-step-title\">Choose your container<\/div><div class=\"nsc-lp-step-copy\">Pick 20ft, 40ft, 40HC, Open Top or Flat Rack.<\/div><\/div><\/div>\r\n<div class=\"nsc-lp-step\"><div class=\"nsc-lp-step-number\">2<\/div><div><div class=\"nsc-lp-step-title\">Add your cargo<\/div><div class=\"nsc-lp-step-copy\">Enter the package size in cm, quantity and kg per piece.<\/div><\/div><\/div>\r\n<div class=\"nsc-lp-step\"><div class=\"nsc-lp-step-number\">3<\/div><div><div class=\"nsc-lp-step-title\">Create the 3D plan<\/div><div class=\"nsc-lp-step-copy\">See what fits, what does not, and how cargo is arranged.<\/div><\/div><\/div>\r\n<\/div>\r\n\r\n<!-- CONTROLS -->\r\n<div class=\"nsc-lp-toolbar\">\r\n\r\n<div class=\"nsc-lp-card nsc-lp-controls\">\r\n\r\n<div class=\"nsc-lp-card-title\"><span class=\"nsc-lp-step-tag\">Step 1<\/span> Choose Your Container<\/div>\r\n<div class=\"nsc-lp-section-intro\">Start here. The selected equipment determines the available internal space and payload reference. In the 3D view, the <strong>orange end is the door \/ opening side<\/strong>; the <strong>blue end is the back \/ inside end<\/strong>.<\/div>\r\n\r\n<div class=\"nsc-lp-field\">\r\n<label for=\"nscContainerType\">Container Type<\/label>\r\n<select id=\"nscContainerType\" class=\"nsc-lp-select\">\r\n<option value=\"20GP\">20' Standard (20GP)<\/option>\r\n<option value=\"40GP\">40' Standard (40GP)<\/option>\r\n<option value=\"40HC\">40' High Cube (40HC)<\/option>\r\n<option value=\"20OT\">20' Open Top<\/option>\r\n<option value=\"20OTHC\">20' Open Top High Cube<\/option>\r\n<option value=\"40OT\">40' Open Top<\/option>\r\n<option value=\"40OTHC\">40' Open Top High Cube<\/option>\r\n<option value=\"20FR\">20' Flat Rack<\/option>\r\n<option value=\"40FRHC\">40' Flat Rack High Cube<\/option>\r\n<\/select>\r\n<div class=\"nsc-lp-help\">For a normal dry container, start with <strong>20GP<\/strong>. Choose <strong>40HC<\/strong> when you need more length or height.<\/div>\r\n<div id=\"nscEquipmentRuleBox\" class=\"nsc-lp-equipment-rule-box\">\r\n<strong>20GP \/ 40GP \/ 40HC rules:<\/strong> Standard enclosed-container planning uses section-first and layer-first loading, grouped by shipper. Select Open Top or Flat Rack to apply their separate OOG rules.\r\n<\/div>\r\n<\/div>\r\n\r\n<details class=\"nsc-lp-advanced\">\r\n<summary>Advanced loading settings<\/summary>\r\n<div class=\"nsc-lp-form-grid\">\r\n\r\n<div class=\"nsc-lp-field\">\r\n<label for=\"nscPackMode\">Cargo Arrangement<\/label>\r\n<select id=\"nscPackMode\" class=\"nsc-lp-select\">\r\n<option value=\"section\">Back-to-door sections<\/option>\r\n<option value=\"balanced\">Balanced load<\/option>\r\n<\/select>\r\n<div class=\"nsc-lp-help\">For 20GP \/ 40GP \/ 40HC this keeps the locked back-to-door section workflow. Open Top and Flat Rack use their own equipment-specific loading rules.<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-field\">\r\n<label for=\"nscDoorCheck\">Fit Check<\/label>\r\n<select id=\"nscDoorCheck\" class=\"nsc-lp-select\">\r\n<option value=\"strict\">Strict<\/option>\r\n<option value=\"relaxed\">Relaxed<\/option>\r\n<\/select>\r\n<div class=\"nsc-lp-help\">Strict is recommended for a safer first planning estimate.<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-field\">\r\n<label for=\"nscGapTolerance\">Edge Matching Tolerance (mm)<\/label>\r\n<input id=\"nscGapTolerance\" class=\"nsc-lp-input\" type=\"number\" min=\"0\" step=\"1\" value=\"2\">\r\n<div class=\"nsc-lp-help\">Small planning allowance used when matching package edges.<\/div>\r\n<\/div>\r\n\r\n<\/div>\r\n<\/details>\r\n\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-card nsc-lp-status-card\">\r\n\r\n<div class=\"nsc-lp-card-title\"><span class=\"nsc-lp-step-tag\">Live<\/span> Plan Summary<\/div>\r\n\r\n<div class=\"nsc-lp-kpi-grid\">\r\n<div class=\"nsc-lp-kpi\">\r\n<div class=\"nsc-lp-kpi-label\">Selected Equipment<\/div>\r\n<div id=\"nscKpiContainer\" class=\"nsc-lp-kpi-value\">20GP<\/div>\r\n<div id=\"nscKpiContainerSub\" class=\"nsc-lp-kpi-small\">5,898 \u00d7 2,352 \u00d7 2,393 mm<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-kpi\">\r\n<div class=\"nsc-lp-kpi-label\">Cargo Pieces<\/div>\r\n<div id=\"nscKpiPieces\" class=\"nsc-lp-kpi-value\">0<\/div>\r\n<div class=\"nsc-lp-kpi-small\">planned packages<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-kpi\">\r\n<div class=\"nsc-lp-kpi-label\">Total Cargo Weight<\/div>\r\n<div id=\"nscKpiWeight\" class=\"nsc-lp-kpi-value\">0 kg<\/div>\r\n<div id=\"nscKpiPayloadSub\" class=\"nsc-lp-kpi-small\">Payload limit 28,300 kg<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-kpi\">\r\n<div class=\"nsc-lp-kpi-label\">Used Volume<\/div>\r\n<div id=\"nscKpiVolume\" class=\"nsc-lp-kpi-value\">0%<\/div>\r\n<div id=\"nscKpiVolumeSub\" class=\"nsc-lp-kpi-small\">0.000 m\u00b3<\/div>\r\n<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-friendly-note\"><strong>How to read this:<\/strong> <strong>Pieces<\/strong> = number of packages. <strong>Weight<\/strong> = total cargo weight. <strong>Used Volume<\/strong> = approximate space used. These are planning figures.<\/div>\r\n\r\n<div id=\"nscMainAlert\" class=\"nsc-lp-alert\">\r\nChoose a container and add cargo. Your results will appear here after you create the 3D plan.\r\n<\/div>\r\n\r\n<\/div>\r\n<\/div>\r\n\r\n<!-- CARGO INPUT TABLE -->\r\n<div class=\"nsc-lp-card nsc-lp-cargo-card\">\r\n\r\n<div class=\"nsc-lp-cargo-step-head\">\r\n<span class=\"nsc-lp-step-tag\">Step 2<\/span>\r\n<div class=\"nsc-lp-cargo-step-copy\">\r\n<div class=\"nsc-lp-card-title\" style=\"margin-bottom:0;\">Add Your Cargo<\/div>\r\n<p>One row = one cargo type. Cargo from different shippers can be loaded into the same container.<\/p>\r\n<div class=\"nsc-lp-cargo-tip\"><strong>Use cm:<\/strong> enter the outside package size, quantity and weight per piece. For the example, each shipper uses exactly one carton size, with different shippers using different carton sizes. For the dry-container example, cartons stay at or below 50 cm per side and follow grouped section\/layer loading. Open Top and Flat Rack examples use their own OOG rules.<\/div>\r\n<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-table-wrap\">\r\n<table class=\"nsc-lp-cargo-table\">\r\n<thead>\r\n<tr>\r\n<th>Shipper<\/th>\r\n<th>Cargo<\/th>\r\n<th>L (cm)<\/th>\r\n<th>W (cm)<\/th>\r\n<th>H (cm)<\/th>\r\n<th>Pieces<\/th>\r\n<th>Weight \/ Piece (kg)<\/th>\r\n<th>Can Turn?<\/th>\r\n<th>Stackable?<\/th>\r\n<th>Rollable?<\/th>\r\n<th>Max Boxes High<\/th>\r\n<th>Gauge Status<\/th>\r\n<th>3D Color<\/th>\r\n<th><\/th>\r\n<\/tr>\r\n<\/thead>\r\n<tbody id=\"nscCargoBody\"><\/tbody>\r\n<\/table>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-cargo-guidance\">\r\n<strong>Handling rules:<\/strong> <strong>Stackable = No<\/strong> prevents the planner from using that cargo as a support for another piece. <strong>Rollable = Yes<\/strong> does not change the volume calculation, but the planner flags the cargo for additional securing \/ chocking review.\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-cargo-actions\">\r\n<div class=\"nsc-lp-cargo-actions-copy\">\r\n<strong>Ready to plan?<\/strong> Add all cargo lots above, then use the buttons below to continue directly to the 3D loading plan. For 20GP \/ 40GP \/ 40HC, the example uses grouped shippers and one carton size per shipper. Open Top and Flat Rack use their own OOG examples and rules. Use Stackable = No for cargo that must not support another package; use Rollable = Yes when the cargo needs extra securing \/ chocking attention.\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-actions\">\r\n<button id=\"nscAddCargo\" class=\"nsc-lp-button\" type=\"button\">+ Add Cargo Lot<\/button>\r\n<button id=\"nscLoadExample\" class=\"nsc-lp-button\" type=\"button\">Load Multi-Shipper Full-Capacity Example<\/button>\r\n<button id=\"nscRunPlanner\" class=\"nsc-lp-button nsc-lp-button-primary\" type=\"button\">Run 3D Loading Plan<\/button>\r\n<button id=\"nscResetPlanner\" class=\"nsc-lp-button\" type=\"button\">Reset<\/button>\r\n<\/div>\r\n<\/div>\r\n\r\n<\/div>\r\n\r\n<!-- 3D + REPORT -->\r\n<div class=\"nsc-lp-view-grid\">\r\n\r\n<div class=\"nsc-lp-card nsc-lp-view-card\">\r\n<div class=\"nsc-lp-view-toolbar\">\r\n<div>\r\n<div class=\"nsc-lp-view-title\"><span class=\"nsc-lp-step-tag\">Step 3<\/span> Your 3D Loading Plan<\/div>\r\n<div class=\"nsc-lp-view-sub\">Move the model to inspect your cargo from different angles.<\/div><div class=\"nsc-lp-view-guide\"><span>Drag = rotate<\/span><span>Scroll = zoom<\/span><span>Right-drag = move<\/span><\/div>\r\n<div class=\"nsc-lp-visual-legend\"><span><i class=\"nsc-lp-legend-dot nsc-lp-legend-ground\"><\/i>Loading yard<\/span><span><i class=\"nsc-lp-legend-dot nsc-lp-legend-frame\"><\/i>Container<\/span><span><i class=\"nsc-lp-legend-dot nsc-lp-legend-cargo\"><\/i>Cargo (colors = shippers)<\/span><\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-view-actions\">\r\n<button id=\"nscViewReset\" class=\"nsc-lp-view-button\" type=\"button\">Reset View<\/button>\r\n<button id=\"nscView360\" class=\"nsc-lp-view-button\" type=\"button\" aria-pressed=\"false\">360\u00b0 Auto View<\/button>\r\n<div id=\"nsc3dBadge\" class=\"nsc-lp-fit-badge\">NOT RUN<\/div>\r\n<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-orientation-strip\" aria-label=\"Container orientation\">\r\n<div class=\"nsc-lp-orientation-side nsc-lp-orientation-inside\"><span class=\"nsc-lp-orientation-dot\"><\/span>Back \/ Inside<\/div>\r\n<div class=\"nsc-lp-orientation-side nsc-lp-orientation-door\"><span class=\"nsc-lp-orientation-dot\"><\/span>Door \/ Opening Side<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-canvas-wrap\">\r\n<canvas id=\"nscLpCanvas\"><\/canvas>\r\n<div class=\"nsc-lp-canvas-note\">\r\nThe 3D view is a planning visualization. Final stuffing must be verified against the actual equipment, CSC plate, carrier limits, cargo securing requirements, center-of-gravity rules and route-specific restrictions.\r\n<\/div>\r\n<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-card nsc-lp-report\">\r\n\r\n<div class=\"nsc-lp-report-section\">\r\n<div class=\"nsc-lp-card-title\">Does the Cargo Fit?<\/div>\r\n\r\n<div class=\"nsc-lp-report-row\">\r\n<span class=\"nsc-lp-report-label\">Cargo placed<\/span>\r\n<strong id=\"nscPlacedPieces\" class=\"nsc-lp-report-value\">0 \/ 0 pcs<\/strong>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-row\">\r\n<span class=\"nsc-lp-report-label\">Cargo not placed<\/span>\r\n<strong id=\"nscUnplacedPieces\" class=\"nsc-lp-report-value\">0 pcs<\/strong>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-row\">\r\n<span class=\"nsc-lp-report-label\">Weight used<\/span>\r\n<strong id=\"nscPayloadValue\" class=\"nsc-lp-report-value\">0 \/ 0 kg<\/strong>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-row\">\r\n<span class=\"nsc-lp-report-label\">Space used<\/span>\r\n<strong id=\"nscVolumeValue\" class=\"nsc-lp-report-value\">0%<\/strong>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-row\">\r\n<span class=\"nsc-lp-report-label\">Weight balance<\/span>\r\n<strong id=\"nscBalanceValue\" class=\"nsc-lp-report-value\">\u2014<\/strong>\r\n<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-section\">\r\n<div class=\"nsc-lp-card-title\">Where Did Each Shipper Go?<\/div>\r\n<div id=\"nscShipperList\" class=\"nsc-lp-shipper-list\"><\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-section\">\r\n<div class=\"nsc-lp-card-title\">Handling Summary<\/div>\r\n<div id=\"nscHandlingSummary\" style=\"color:#687989;font-size:9px;line-height:1.6;\">Stackable \/ rollable status will appear after you create the plan.<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-section\">\r\n<div class=\"nsc-lp-card-title\">Things to Review<\/div>\r\n<div id=\"nscWarnings\" class=\"nsc-lp-alert\">No plan has been generated.<\/div>\r\n<\/div>\r\n\r\n<div class=\"nsc-lp-report-section\">\r\n<div class=\"nsc-lp-card-title\">How the Planner Works<\/div>\r\n<div style=\"color:#687989;font-size:9px;line-height:1.6;\">\r\nHeavier packages are prioritized toward lower layers. Stackable cargo may support higher layers; non-stackable cargo is never used as a support surface. Rollable cargo is flagged for securing review. The planner checks cargo overlap, container boundaries and available support. It is a planning aid, not a guarantee of an optimal or carrier-approved stuffing plan.\r\n<\/div>\r\n<\/div>\r\n\r\n<\/div>\r\n<\/div>\r\n\r\n<!-- DATABASE -->\r\n<div class=\"nsc-lp-card nsc-lp-database-card\">\r\n<div class=\"nsc-lp-card-title\">Container Specifications \u2014 Planning Reference<\/div>\r\n<div id=\"nscDatabaseGrid\" class=\"nsc-lp-database-grid\"><\/div>\r\n<div class=\"nsc-lp-disclaimer\">\r\nThe specifications below are planning references only. Equipment dimensions can vary by manufacturer, series and carrier fleet. For an actual shipment, confirm the physical container, payload, door\/opening limits, cargo securing requirements and route-specific restrictions before stuffing.\r\n<\/div>\r\n<\/div>\r\n\r\n<\/div>\r\n<\/div>\r\n<\/section>\r\n\r\n<\/div>\r\n\r\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.149.0\/build\/three.min.js\"><\/script>\r\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.149.0\/examples\/js\/controls\/OrbitControls.js\"><\/script>\r\n\r\n<script>\r\n(function(){\r\n\"use strict\";\r\n\r\n\/* =========================================================\r\n   NSC SINGLE-INSTANCE \/ ELEMENTOR DUPLICATE GUARD\r\n   ---------------------------------------------------------\r\n   Elementor can sometimes render the same HTML widget\/template\r\n   more than once. The previous singleton stopped the second\r\n   JavaScript execution, but left the second HTML in the DOM.\r\n   This guard removes every duplicate planner root and keeps only\r\n   the first live instance.\r\n========================================================= *\/\r\nconst ROOT_SELECTOR='[data-nsc-load-planner-root=\"true\"]';\r\nconst GLOBAL_ROOT_KEY=\"__NSC_3D_LOAD_PLANNER_ROOT_V2__\";\r\nconst GLOBAL_KEY=\"__NSC_3D_LOAD_PLANNER_V2_SINGLETON__\";\r\n\r\nfunction cleanupDuplicateRoots(){\r\n    const roots=Array.from(document.querySelectorAll(ROOT_SELECTOR));\r\n    if(!roots.length)return null;\r\n\r\n    let keeper=window[GLOBAL_ROOT_KEY];\r\n    if(!keeper || !keeper.isConnected){\r\n        keeper=roots[0];\r\n        window[GLOBAL_ROOT_KEY]=keeper;\r\n    }\r\n\r\n    roots.forEach(function(el){\r\n        if(el!==keeper)el.remove();\r\n    });\r\n\r\n    return keeper;\r\n}\r\n\r\nconst root=cleanupDuplicateRoots();\r\nif(!root)return;\r\n\r\n\/* A second copy may be injected after the first script has already run.\r\n   The observer removes any later duplicate root as soon as it appears. *\/\r\nif(!window.__NSC_3D_LOAD_PLANNER_DUPLICATE_OBSERVER__){\r\n    const observer=new MutationObserver(function(){\r\n        cleanupDuplicateRoots();\r\n    });\r\n    const observeTarget=document.body || document.documentElement;\r\n    if(observeTarget){\r\n        observer.observe(observeTarget,{childList:true,subtree:true});\r\n        window.__NSC_3D_LOAD_PLANNER_DUPLICATE_OBSERVER__=observer;\r\n    }\r\n}\r\n\r\nif(window[GLOBAL_KEY]) return;\r\nwindow[GLOBAL_KEY]=true;\r\n\r\n\/* =========================================================\r\n   EQUIPMENT DATABASE\r\n   Planning references based on carrier examples.\r\n========================================================= *\/\r\nconst EQUIPMENT={\r\n    \"20GP\":{\r\n        label:\"20' Standard \/ 20GP\",\r\n        type:\"dry\",\r\n        length:5898,\r\n        width:2352,\r\n        height:2393,\r\n        payload:28300,\r\n        doorWidth:2340,\r\n        doorHeight:2280,\r\n        capacity:33.2\r\n    },\r\n    \"40GP\":{\r\n        label:\"40' Standard \/ 40GP\",\r\n        type:\"dry\",\r\n        length:12032,\r\n        width:2352,\r\n        height:2393,\r\n        payload:28870,\r\n        doorWidth:2340,\r\n        doorHeight:2280,\r\n        capacity:67.7\r\n    },\r\n    \"40HC\":{\r\n        label:\"40' High Cube \/ 40HC\",\r\n        type:\"dry-hc\",\r\n        length:12032,\r\n        width:2352,\r\n        height:2698,\r\n        payload:28690,\r\n        doorWidth:2340,\r\n        doorHeight:2585,\r\n        capacity:76.4\r\n    },\r\n    \"20OT\":{\r\n        label:\"20' Open Top\",\r\n        type:\"ot\",\r\n        length:5895,\r\n        width:2350,\r\n        height:2340,\r\n        heightNoBars:2340,\r\n        payload:30050,\r\n        doorWidth:2338,\r\n        doorHeight:2280,\r\n        roofWidth:2230,\r\n        roofLength:5338,\r\n        capacity:32.5\r\n    },\r\n    \"20OTHC\":{\r\n        label:\"20' Open Top High Cube\",\r\n        type:\"ot-hc\",\r\n        length:5892,\r\n        width:2347,\r\n        height:2648,\r\n        heightNoBars:2720,\r\n        payload:27970,\r\n        doorWidth:2335,\r\n        doorHeight:2580,\r\n        roofWidth:2227,\r\n        roofLength:5391,\r\n        capacity:36.8\r\n    },\r\n    \"40OT\":{\r\n        label:\"40' Open Top\",\r\n        type:\"ot\",\r\n        length:12029,\r\n        width:2350,\r\n        height:2344,\r\n        heightNoBars:2420,\r\n        payload:28450,\r\n        doorWidth:2340,\r\n        doorHeight:2276,\r\n        roofWidth:2230,\r\n        roofLength:11552,\r\n        capacity:66.8\r\n    },\r\n    \"40OTHC\":{\r\n        label:\"40' Open Top High Cube\",\r\n        type:\"ot-hc\",\r\n        length:12029,\r\n        width:2352,\r\n        height:2649,\r\n        heightNoBars:2720,\r\n        payload:28250,\r\n        doorWidth:2352,\r\n        doorHeight:2570,\r\n        roofWidth:2192,\r\n        roofLength:11552,\r\n        capacity:74.9\r\n    },\r\n    \"20FR\":{\r\n        label:\"20' Flat Rack\",\r\n        type:\"fr\",\r\n        length:5638,\r\n        width:2438,\r\n        minWidth:2194,\r\n        height:2233,\r\n        deckHeight:370,\r\n        payload:42100,\r\n        capacity:null\r\n    },\r\n    \"40FRHC\":{\r\n        label:\"40' Flat Rack High Cube\",\r\n        type:\"fr-hc\",\r\n        length:11652,\r\n        width:2347,\r\n        minWidth:2245,\r\n        height:2265,\r\n        deckHeight:648,\r\n        payload:49100,\r\n        capacity:null\r\n    }\r\n};\r\n\r\n\/* =========================================================\r\n   SHIPPER COLORS\r\n========================================================= *\/\r\nconst SHIPPER_COLORS=[\r\n    \"#7a4aa8\",\r\n    \"#16855b\",\r\n    \"#d97706\",\r\n    \"#b53026\",\r\n    \"#008a99\",\r\n    \"#6d7f23\",\r\n    \"#a34f7c\",\r\n    \"#8b5e3c\"\r\n];\r\n\r\nconst cargoRows=[];\r\nlet rowSequence=1;\r\nlet scene=null;\r\nlet camera=null;\r\nlet renderer=null;\r\nlet controls=null;\r\nlet containerGroup=null;\r\nlet cargoGroup=null;\r\nlet sectionGroup=null;\r\nlet renderMode=\"none\";\r\nlet currentPlan=null;\r\nlet exampleMode=false;\r\nlet exampleBlueprint=null;\r\nlet fallbackCtx=null;\r\nlet fallbackCanvas=null;\r\nlet fallbackResizeObserver=null;\r\nconst fallbackCamera={yaw:-0.65,pitch:0.38,zoom:1,panX:0,panY:0};\r\nlet fallbackAutoRotate=false;\r\nlet fallbackAnimationFrame=null;\r\n\r\n\/* Visual-only yard\/container separation. Packing coordinates remain unchanged. *\/\r\nconst NSC_GROUND_CLEARANCE=1.10;\r\nconst NSC_FLAT_RACK_END_CLEARANCE=450;\r\n\r\n\/* =========================================================\r\n   DOM HELPERS\r\n========================================================= *\/\r\nfunction byId(id){return root.querySelector(\"#\"+id);}\r\nfunction fmt(value,d=2){\r\n    return Number(value||0).toLocaleString(\"en-US\",{\r\n        minimumFractionDigits:d,\r\n        maximumFractionDigits:d\r\n    });\r\n}\r\nfunction safeNum(v){\r\n    const n=Number(v);\r\n    return Number.isFinite(n)?n:0;\r\n}\r\nfunction clamp(v,a,b){return Math.max(a,Math.min(b,v));}\r\n\r\n\/* =========================================================\r\n   CARGO ROWS\r\n========================================================= *\/\r\nfunction createCargoRow(data={}){\r\n    const id=rowSequence++;\r\n    const color=data.color||SHIPPER_COLORS[(id-1)%SHIPPER_COLORS.length];\r\n    const row={\r\n        id:id,\r\n        shipper:data.shipper||\"Shipper \"+id,\r\n        description:data.description||\"Carton\",\r\n        length:safeNum(data.length)||1200,\r\n        width:safeNum(data.width)||800,\r\n        height:safeNum(data.height)||1000,\r\n        qty:Math.max(1,Math.floor(safeNum(data.qty)||1)),\r\n        weight:safeNum(data.weight)||25,\r\n        rotation:data.rotation||\"all\",\r\n        stackable:data.stackable!==false,\r\n        rollable:data.rollable===true,\r\n        maxStack:data.stackable===false ? 1 : Math.max(1,Math.floor(safeNum(data.maxStack)||99)),\r\n        color:color\r\n    };\r\n    cargoRows.push(row);\r\n    renderCargoTable();\r\n}\r\n\r\nfunction removeCargoRow(id){\r\n    exampleMode=false;\r\n    exampleBlueprint=null;\r\n    const index=cargoRows.findIndex(r=>r.id===id);\r\n    if(index>=0){\r\n        cargoRows.splice(index,1);\r\n        renderCargoTable();\r\n    }\r\n}\r\n\r\nfunction renderCargoTable(){\r\n    const body=byId(\"nscCargoBody\");\r\n    if(!body)return;\r\n    body.innerHTML=\"\";\r\n\r\n    cargoRows.forEach(function(row){\r\n        const tr=document.createElement(\"tr\");\r\n        tr.dataset.id=String(row.id);\r\n        tr.innerHTML=`\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"shipper\" value=\"${escapeHtml(row.shipper)}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"description\" value=\"${escapeHtml(row.description)}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"length\" type=\"number\" min=\"0.1\" step=\"0.1\" value=\"${row.length\/10}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"width\" type=\"number\" min=\"0.1\" step=\"0.1\" value=\"${row.width\/10}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"height\" type=\"number\" min=\"0.1\" step=\"0.1\" value=\"${row.height\/10}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"qty\" type=\"number\" min=\"1\" step=\"1\" value=\"${row.qty}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"weight\" type=\"number\" min=\"0\" step=\"0.01\" value=\"${row.weight}\"><\/td>\r\n            <td>\r\n                <select class=\"nsc-lp-mini-select\" data-field=\"rotation\">\r\n                    <option value=\"all\" ${row.rotation===\"all\"?\"selected\":\"\"}>Any<\/option>\r\n                    <option value=\"no-rotate\" ${row.rotation===\"no-rotate\"?\"selected\":\"\"}>No Rotate<\/option>\r\n                    <option value=\"floor\" ${row.rotation===\"floor\"?\"selected\":\"\"}>Floor Only<\/option>\r\n                <\/select>\r\n            <\/td>\r\n            <td>\r\n                <select class=\"nsc-lp-mini-select\" data-field=\"stackable\">\r\n                    <option value=\"yes\" ${row.stackable!==false?\"selected\":\"\"}>Yes<\/option>\r\n                    <option value=\"no\" ${row.stackable===false?\"selected\":\"\"}>No<\/option>\r\n                <\/select>\r\n            <\/td>\r\n            <td>\r\n                <select class=\"nsc-lp-mini-select\" data-field=\"rollable\">\r\n                    <option value=\"no\" ${row.rollable!==true?\"selected\":\"\"}>No<\/option>\r\n                    <option value=\"yes\" ${row.rollable===true?\"selected\":\"\"}>Yes<\/option>\r\n                <\/select>\r\n            <\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"maxStack\" type=\"number\" min=\"1\" step=\"1\" value=\"${row.maxStack}\" ${row.stackable===false?\"disabled\":\"\"}><\/td>\r\n            <td><input class=\"nsc-lp-color\" data-field=\"color\" type=\"color\" value=\"${row.color}\"><\/td>\r\n            <td><button class=\"nsc-lp-row-remove\" type=\"button\" title=\"Remove\">\u00d7<\/button><\/td>\r\n        `;\r\n        body.appendChild(tr);\r\n    });\r\n\r\n    body.querySelectorAll(\"tr\").forEach(function(tr){\r\n        const row=cargoRows.find(r=>String(r.id)===tr.dataset.id);\r\n        if(!row)return;\r\n        tr.querySelectorAll(\"[data-field]\").forEach(function(input){\r\n            input.addEventListener(\"input\",function(){\r\n                exampleMode=false;\r\n                exampleBlueprint=null;\r\n                const field=input.dataset.field;\r\n                if(field===\"shipper\"||field===\"description\"||field===\"rotation\"||field===\"color\"){\r\n                    row[field]=input.value;\r\n                }else if(field===\"stackable\"){\r\n                    row.stackable=input.value!==\"no\";\r\n                    if(!row.stackable){\r\n                        row.maxStack=1;\r\n                    } else if(row.maxStack<1){\r\n                        row.maxStack=99;\r\n                    }\r\n                    renderCargoTable();\r\n                }else if(field===\"rollable\"){\r\n                    row.rollable=input.value===\"yes\";\r\n                }else if(field===\"qty\"||field===\"maxStack\"){\r\n                    row[field]=Math.max(1,Math.floor(safeNum(input.value)||1));\r\n                }else{\r\n                    \/* UI dimensions are in CM. Keep the packing engine in MM. *\/\r\n                    if(field===\"length\"||field===\"width\"||field===\"height\") {\r\n                        row[field]=safeNum(input.value)*10;\r\n                    } else {\r\n                        row[field]=safeNum(input.value);\r\n                    }\r\n                }\r\n                updateTopKPIs();\r\n            });\r\n        });\r\n        const remove=tr.querySelector(\".nsc-lp-row-remove\");\r\n        remove.addEventListener(\"click\",function(){removeCargoRow(row.id);});\r\n    });\r\n\r\n    updateTopKPIs();\r\n}\r\n\r\nfunction escapeHtml(value){\r\n    return String(value??\"\")\r\n        .replace(\/&\/g,\"&amp;\")\r\n        .replace(\/<\/g,\"&lt;\")\r\n        .replace(\/>\/g,\"&gt;\")\r\n        .replace(\/\"\/g,\"&quot;\")\r\n        .replace(\/'\/g,\"&#039;\");\r\n}\r\n\r\n\/* =========================================================\r\n   EQUIPMENT UI\r\n========================================================= *\/\r\nfunction currentEquipment(){\r\n    return EQUIPMENT[byId(\"nscContainerType\").value]||EQUIPMENT[\"20GP\"];\r\n}\r\n\r\nfunction updateEquipmentUI(){\r\n    const key=byId(\"nscContainerType\").value;\r\n    const e=currentEquipment();\r\n    byId(\"nscKpiContainer\").textContent=key;\r\n    byId(\"nscKpiContainerSub\").textContent=`${fmt(e.length,0)} \u00d7 ${fmt(e.width,0)} \u00d7 ${fmt(e.height,0)} mm`;\r\n    byId(\"nscKpiPayloadSub\").textContent=`Payload limit ${fmt(e.payload,0)} kg`;\r\n    byId(\"nscKpiVolumeSub\").textContent=e.capacity?`${fmt(e.capacity,1)} m\u00b3 nominal`:\"Open \/ OOG profile\";\r\n    renderDatabase();\r\n}\r\n\r\nfunction renderDatabase(){\r\n    const grid=byId(\"nscDatabaseGrid\");\r\n    if(!grid)return;\r\n    grid.innerHTML=\"\";\r\n    Object.keys(EQUIPMENT).forEach(function(key){\r\n        const e=EQUIPMENT[key];\r\n        const div=document.createElement(\"div\");\r\n        div.className=\"nsc-lp-db-item\";\r\n        const d1=`${fmt(e.length,0)} \u00d7 ${fmt(e.width,0)} \u00d7 ${fmt(e.height,0)} mm`;\r\n        const d2=`Payload ${fmt(e.payload,0)} kg`;\r\n        let extra=\"\";\r\n        if(e.doorWidth)extra+=`Door ${fmt(e.doorWidth,0)} \u00d7 ${fmt(e.doorHeight,0)} mm`;\r\n        if(e.roofWidth)extra+=(extra?\" \u00b7 \":\"\")+`Roof ${fmt(e.roofLength,0)} \u00d7 ${fmt(e.roofWidth,0)} mm`;\r\n        if(e.minWidth)extra+=(extra?\" \u00b7 \":\"\")+`Min width ${fmt(e.minWidth,0)} mm`;\r\n        div.innerHTML=`<strong>${escapeHtml(key)} \u00b7 ${escapeHtml(e.label)}<\/strong><span>${d1}<br>${d2}${extra?\"<br>\"+extra:\"\"}<\/span>`;\r\n        grid.appendChild(div);\r\n    });\r\n}\r\n\r\n\/* =========================================================\r\n   KPI\r\n========================================================= *\/\r\nfunction getTotalRequestedPieces(){\r\n    return cargoRows.reduce((sum,r)=>sum+r.qty,0);\r\n}\r\nfunction getTotalRequestedWeight(){\r\n    return cargoRows.reduce((sum,r)=>sum+(r.qty*r.weight),0);\r\n}\r\nfunction getTotalRequestedVolume(){\r\n    return cargoRows.reduce((sum,r)=>sum+(r.qty*r.length*r.width*r.height\/1000000000),0);\r\n}\r\nfunction updateTopKPIs(extra={}){\r\n    const e=currentEquipment();\r\n    const pieces=getTotalRequestedPieces();\r\n    const weight=getTotalRequestedWeight();\r\n    const volume=getTotalRequestedVolume();\r\n    const pct=e.capacity?Math.min(999,(volume\/e.capacity)*100):0;\r\n    byId(\"nscKpiPieces\").textContent=fmt(pieces,0);\r\n    byId(\"nscKpiWeight\").textContent=`${fmt(weight,2)} kg`;\r\n    byId(\"nscKpiVolume\").textContent=`${fmt(pct,1)}%`;\r\n    if(extra.placed!==undefined){\r\n        byId(\"nscPlacedPieces\").textContent=`${fmt(extra.placed,0)} \/ ${fmt(pieces,0)} pcs`;\r\n        byId(\"nscUnplacedPieces\").textContent=`${fmt(extra.unplaced||0,0)} pcs`;\r\n        byId(\"nscPayloadValue\").textContent=`${fmt(extra.placedWeight||0,2)} \/ ${fmt(e.payload,0)} kg`;\r\n        byId(\"nscVolumeValue\").textContent=`${fmt(extra.usedVolumePct||0,1)}%`;\r\n        byId(\"nscBalanceValue\").textContent=extra.balance||\"\u2014\";\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   EXAMPLE \/ RESET\r\n========================================================= *\/\r\nfunction clearCargoRows(){\r\n    exampleBlueprint=null;\r\n    cargoRows.length=0;\r\n    renderCargoTable();\r\n}\r\n\r\nfunction makeTwoSizeSegments(total, maxSize=500, minSize=300){\r\n    total=safeNum(total);\r\n    if(total<=0)return [];\r\n\r\n    const count=Math.max(1,Math.ceil(total\/maxSize));\r\n\r\n    if(count===1){\r\n        return [total];\r\n    }\r\n\r\n    const highCount=Math.max(1,Math.floor(count\/2));\r\n    const high=Math.min(maxSize,Math.max(minSize,total\/count + (maxSize-total\/count)*0.55));\r\n    const low=(total-(high*highCount))\/(count-highCount);\r\n\r\n    \/* If the calculated low side falls outside the safe planning range,\r\n       fall back to an even split. *\/\r\n    if(low<minSize || low>maxSize){\r\n        const even=total\/count;\r\n        return Array.from({length:count},function(_,index){\r\n            return index===count-1\r\n                ? total-(even*(count-1))\r\n                : even;\r\n        });\r\n    }\r\n\r\n    const segments=[];\r\n    for(let i=0;i<highCount;i++)segments.push(high);\r\n    for(let i=highCount;i<count;i++)segments.push(low);\r\n    return segments;\r\n}\r\n\r\nfunction isStructuredSectionExample(){\r\n    return exampleMode && Array.isArray(exampleBlueprint) && exampleBlueprint.length>0;\r\n}\r\n\r\nfunction loadExample(){\r\n    \/*\r\n       MULTI-SHIPPER \/ ONE-CARTON-SIZE-EACH \/ MAX-CAPACITY \/ LIGHTWEIGHT EXAMPLE\r\n       -------------------------------------------------------------------------\r\n       - Maximum 4 shippers in the example (never more than 5).\r\n       - Each shipper has exactly ONE carton size and ONE cargo row.\r\n       - Each carton dimension is <= 500 mm \/ 50 cm.\r\n       - Carton dimensions are chosen as large as practical for the allocated\r\n         loading block while still fully tiling that block without gaps.\r\n       - Loading order is section-major, then layer-major, then back-to-front:\r\n         Section 1 floor -> top, then Section 2 floor -> top, then Section 3,\r\n         then Section 4 toward the door.\r\n       - Each shipper remains grouped and is never reintroduced after the next\r\n         shipper begins.\r\n       - This example is intentionally simple so it remains responsive even\r\n         for larger 40 ft \/ 40HC equipment.\r\n    *\/\r\n\r\n    clearCargoRows();\r\n    exampleMode=true;\r\n    exampleBlueprint=[];\r\n\r\n    const e=currentEquipment();\r\n    const bounds=boundsFor(e);\r\n    const ranges=sectionRanges(e);\r\n    const H=bounds.H;\r\n\r\n    const shippers=[\r\n        {\r\n            name:\"Shipper A\",\r\n            description:\"Consumer Goods Cartons\",\r\n            weight:6.5,\r\n            stackable:true,\r\n            rollable:false,\r\n            color:SHIPPER_COLORS[0]\r\n        },\r\n        {\r\n            name:\"Shipper B\",\r\n            description:\"Automotive Parts Cartons\",\r\n            weight:7.5,\r\n            stackable:true,\r\n            rollable:false,\r\n            color:SHIPPER_COLORS[1]\r\n        },\r\n        {\r\n            name:\"Shipper C\",\r\n            description:\"Industrial Components Cartons\",\r\n            weight:8.5,\r\n            stackable:true,\r\n            rollable:false,\r\n            color:SHIPPER_COLORS[2]\r\n        },\r\n        {\r\n            name:\"Shipper D\",\r\n            description:\"Electrical Components Cartons\",\r\n            weight:7.0,\r\n            stackable:true,\r\n            rollable:false,\r\n            color:SHIPPER_COLORS[3]\r\n        }\r\n    ];\r\n\r\n    \/*\r\n       The example uses four physical loading sections so the requested\r\n       section-first workflow is preserved exactly.\r\n\r\n       To make each shipper visually different while keeping the carton count\r\n       controlled, each section uses the smallest integer grid that keeps every\r\n       carton side <= 500 mm, with one axis optionally increased by one cell.\r\n       This produces one carton size per shipper and still fills each block exactly.\r\n    *\/\r\n    const sectionGridVariants=[\r\n        [0,0,0],\r\n        [1,0,0],\r\n        [0,1,0],\r\n        [0,0,1]\r\n    ];\r\n\r\n    const cells=[];\r\n    const totals=[];\r\n\r\n    for(let section=0;section<4;section++){\r\n        const shipper=shippers[section];\r\n        const sectionStart=ranges[section];\r\n        const sectionLength=ranges[section+1]-ranges[section];\r\n\r\n        const baseNX=Math.max(1,Math.ceil(sectionLength\/500));\r\n        const baseNZ=Math.max(1,Math.ceil(e.width\/500));\r\n        const baseNY=Math.max(1,Math.ceil(H\/500));\r\n\r\n        const variant=sectionGridVariants[section];\r\n        const nx=baseNX+variant[0];\r\n        const nz=baseNZ+variant[1];\r\n        const ny=baseNY+variant[2];\r\n\r\n        const boxL=sectionLength\/nx;\r\n        const boxW=e.width\/nz;\r\n        const boxH=H\/ny;\r\n\r\n        \/*\r\n           These dimensions are intentionally derived from the allocated\r\n           section, so the resulting carton grid fills the section exactly.\r\n           The largest carton-side rule is 500 mm \/ 50 cm.\r\n        *\/\r\n        const totalPieces=nx*nz*ny;\r\n\r\n        totals.push({\r\n            shipper:shipper,\r\n            length:boxL,\r\n            width:boxW,\r\n            height:boxH,\r\n            count:totalPieces,\r\n            sectionStart:sectionStart,\r\n            nx:nx,\r\n            nz:nz,\r\n            ny:ny\r\n        });\r\n\r\n        for(let layer=0;layer<ny;layer++){\r\n            const y=layer*boxH;\r\n\r\n            for(let zIndex=0;zIndex<nz;zIndex++){\r\n                const z=zIndex*boxW;\r\n\r\n                for(let xIndex=0;xIndex<nx;xIndex++){\r\n                    const x=sectionStart+(xIndex*boxL);\r\n\r\n                    cells.push({\r\n                        order:cells.length,\r\n                        section:section,\r\n                        layer:layer,\r\n                        x:x,\r\n                        y:y,\r\n                        z:z,\r\n                        l:boxL,\r\n                        w:boxW,\r\n                        h:boxH,\r\n                        shipper:shipper.name,\r\n                        shipperIndex:section,\r\n                        nx:nx,\r\n                        nz:nz,\r\n                        ny:ny\r\n                    });\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    \/* Exactly one cargo row per shipper. *\/\r\n    const rowIds={};\r\n\r\n    totals.forEach(function(item){\r\n        createCargoRow({\r\n            shipper:item.shipper.name,\r\n            description:item.shipper.description,\r\n            length:item.length,\r\n            width:item.width,\r\n            height:item.height,\r\n            qty:item.count,\r\n            weight:item.shipper.weight,\r\n            color:item.shipper.color,\r\n            stackable:item.shipper.stackable,\r\n            rollable:item.shipper.rollable,\r\n            maxStack:item.shipper.stackable ? item.ny : 1,\r\n            rotation:\"no-rotate\"\r\n        });\r\n\r\n        const row=cargoRows[cargoRows.length-1];\r\n        rowIds[item.shipper.name]=row.id;\r\n    });\r\n\r\n    cells.forEach(function(cell){\r\n        cell.rowId=rowIds[cell.shipper];\r\n        exampleBlueprint.push(cell);\r\n    });\r\n\r\n    const totalPieces=exampleBlueprint.length;\r\n    const totalWeight=cargoRows.reduce(function(sum,row){\r\n        return sum+(row.qty*row.weight);\r\n    },0);\r\n\r\n    const maxBox=Math.max.apply(null,\r\n        totals.map(function(item){\r\n            return Math.max(item.length,item.width,item.height);\r\n        })\r\n    );\r\n\r\n    const minBox=Math.min.apply(null,\r\n        totals.map(function(item){\r\n            return Math.min(item.length,item.width,item.height);\r\n        })\r\n    );\r\n\r\n    const sizeSummary=totals.map(function(item){\r\n        return item.shipper.name+\" \" +\r\n            fmt(item.length\/10,1)+\"\u00d7\"+\r\n            fmt(item.width\/10,1)+\"\u00d7\"+\r\n            fmt(item.height\/10,1)+\" cm\";\r\n    }).join(\" \u00b7 \");\r\n\r\n    byId(\"nscMainAlert\").className=\"nsc-lp-alert success\";\r\n    byId(\"nscMainAlert\").textContent=\r\n        `Example loaded for ${e.label}: ${fmt(totalPieces,0)} small cargo pieces using ${shippers.length} grouped shippers. Each shipper uses exactly one carton size. Sizes: ${sizeSummary}. No carton dimension exceeds ${fmt(maxBox\/10,1)} cm. Loading sequence: Section 1 floor-to-top \u2192 Section 2 floor-to-top \u2192 Section 3 floor-to-top \u2192 Section 4 floor-to-top toward the door.`;\r\n\r\n    byId(\"nscKpiPieces\").textContent=fmt(totalPieces,0);\r\n    byId(\"nscKpiWeight\").textContent=`${fmt(totalWeight,2)} kg`;\r\n    byId(\"nscKpiVolume\").textContent=\"100.0%\";\r\n    byId(\"nscKpiVolumeSub\").textContent=\"Full geometric envelope \u00b7 1 carton size per shipper\";\r\n    byId(\"nscKpiPayloadSub\").textContent=`Payload limit ${fmt(e.payload,0)} kg`;\r\n    byId(\"nscKpiPiecesSub\").textContent=\"4-shipper lightweight demonstration\";\r\n    byId(\"nscKpiWeightSub\").textContent=`Example carton side range ${fmt(minBox\/10,1)}\u2013${fmt(maxBox\/10,1)} cm`;\r\n}\r\n\r\nfunction resetPlanner(){\r\n    exampleMode=false;\r\n    clearCargoRows();\r\n    createCargoRow();\r\n    byId(\"nscContainerType\").value=\"20GP\";\r\n    byId(\"nscPackMode\").value=\"section\";\r\n    byId(\"nscDoorCheck\").value=\"strict\";\r\n    byId(\"nscGapTolerance\").value=\"2\";\r\n    updateEquipmentUI();\r\n    currentPlan=null;\r\n    stopFallbackAutoRotate();\r\n    setViewControlState(false);\r\n    destroySceneObjects();\r\n    reset3DView();\r\n    resetReport();\r\n}\r\n\r\n\/* =========================================================\r\n   3D SETUP\r\n========================================================= *\/\r\nfunction getCanvasSize(canvas){\r\n    const parent=canvas&&canvas.parentElement;\r\n    let w=canvas&&canvas.clientWidth?canvas.clientWidth:0;\r\n    let h=canvas&&canvas.clientHeight?canvas.clientHeight:0;\r\n    if(!w && parent)w=parent.clientWidth||0;\r\n    if(!h && parent)h=parent.clientHeight||0;\r\n    if(!w)w=800;\r\n    if(!h)h=600;\r\n    return {w:Math.max(1,w),h:Math.max(1,h)};\r\n}\r\n\r\nfunction init3D(){\r\n    const canvas=byId(\"nscLpCanvas\");\r\n    if(!canvas)return;\r\n\r\n    canvas.style.display=\"block\";\r\n    canvas.style.width=\"100%\";\r\n    canvas.style.height=\"100%\";\r\n    canvas.style.touchAction=\"none\";\r\n\r\n    \/* First try native Three.js \/ OrbitControls. If the CDN is unavailable,\r\n       the standalone canvas 3D renderer below keeps the planner functional. *\/\r\n    let threeReady=false;\r\n    try{\r\n        threeReady=!!(\r\n            window.THREE &&\r\n            typeof THREE.WebGLRenderer===\"function\" &&\r\n            typeof THREE.OrbitControls===\"function\"\r\n        );\r\n    }catch(error){\r\n        threeReady=false;\r\n    }\r\n\r\n    if(threeReady){\r\n        try{\r\n            renderer=new THREE.WebGLRenderer({\r\n                canvas:canvas,\r\n                antialias:true,\r\n                alpha:true,\r\n                powerPreference:\"high-performance\"\r\n            });\r\n            renderer.setPixelRatio(Math.min(window.devicePixelRatio||1,2));\r\n            if(\"outputEncoding\" in renderer && THREE.sRGBEncoding)renderer.outputEncoding=THREE.sRGBEncoding;\r\n\r\n            scene=new THREE.Scene();\r\n            scene.background=new THREE.Color(0xe8eef3);\r\n\r\n            camera=new THREE.OrthographicCamera(-5000,5000,3500,-3500,1,50000);\r\n            camera.position.set(7600,5000,8600);\r\n\r\n            controls=new THREE.OrbitControls(camera,canvas);\r\n            controls.enableDamping=true;\r\n            controls.dampingFactor=.07;\r\n            controls.enablePan=true;\r\n            controls.enableRotate=true;\r\n            controls.screenSpacePanning=true;\r\n            controls.minZoom=.55;\r\n            controls.maxZoom=8;\r\n            controls.minPolarAngle=.08;\r\n            controls.maxPolarAngle=Math.PI-.08;\r\n            controls.target.set(0,1000,0);\r\n\r\n            const hemi=new THREE.HemisphereLight(0xffffff,0x64748b,2.1);\r\n            scene.add(hemi);\r\n            const dir1=new THREE.DirectionalLight(0xffffff,1.2);\r\n            dir1.position.set(6000,8000,5000);\r\n            scene.add(dir1);\r\n            const dir2=new THREE.DirectionalLight(0xffffff,.7);\r\n            dir2.position.set(-5000,4000,-4000);\r\n            scene.add(dir2);\r\n\r\n            \/* Neutral environment floor keeps the canvas clean. *\/\r\n            const ground=new THREE.Mesh(\r\n                new THREE.PlaneGeometry(50000,50000),\r\n                new THREE.MeshStandardMaterial({color:0xe7eef4,roughness:1})\r\n            );\r\n            ground.rotation.x=-Math.PI\/2;\r\n            ground.position.y=-(NSC_GROUND_CLEARANCE+0.45);\r\n            scene.add(ground);\r\n\r\n            \/* Bright NS Continent loading-yard floor around the container.\r\n               This is intentionally separate from the blue container floor. *\/\r\n            const yardMaterial=new THREE.MeshStandardMaterial({\r\n                color:0x69c6ee,\r\n                roughness:.88,\r\n                metalness:.02\r\n            });\r\n            const yard=new THREE.Mesh(\r\n                new THREE.BoxGeometry(\r\n                    Math.max(12,L*1.72),\r\n                    .08,\r\n                    Math.max(8,W*1.72)\r\n                ),\r\n                yardMaterial\r\n            );\r\n            yard.position.y=-NSC_GROUND_CLEARANCE;\r\n            scene.add(yard);\r\n\r\n            const yardEdge=new THREE.LineSegments(\r\n                new THREE.EdgesGeometry(yard.geometry),\r\n                new THREE.LineBasicMaterial({\r\n                    color:0xc9a227,\r\n                    transparent:true,\r\n                    opacity:.82\r\n                })\r\n            );\r\n            yardEdge.position.copy(yard.position);\r\n            scene.add(yardEdge);\r\n\r\n            const yardGrid=new THREE.GridHelper(\r\n                Math.max(12,L*1.72),\r\n                12,\r\n                0x0b4f8a,\r\n                0x8ed8f5\r\n            );\r\n            yardGrid.scale.z=Math.max(8,W*1.72)\/Math.max(12,L*1.72);\r\n            yardGrid.position.y=-(NSC_GROUND_CLEARANCE-0.05);\r\n            yardGrid.material.transparent=true;\r\n            yardGrid.material.opacity=.24;\r\n            scene.add(yardGrid);\r\n\r\n            containerGroup=new THREE.Group();\r\n            cargoGroup=new THREE.Group();\r\n            sectionGroup=new THREE.Group();\r\n            scene.add(containerGroup);\r\n            scene.add(sectionGroup);\r\n            scene.add(cargoGroup);\r\n\r\n            renderMode=\"three\";\r\n            if (typeof specialRenderContainer === \"function\" && !isDryEquipment(currentEquipment())) {\r\n                specialRenderContainer();\r\n            } else {\r\n                renderContainer();\r\n            }\r\n            resize3D();\r\n            controls.update();\r\n            requestAnimationFrame(function(){resize3D();});\r\n            window.setTimeout(resize3D,120);\r\n            window.setTimeout(resize3D,600);\r\n            window.addEventListener(\"resize\",resize3D,{passive:true});\r\n            observeCanvasResize();\r\n            animate3D();\r\n            return;\r\n        }catch(error){\r\n            console.warn(\"NSC 3D WebGL initialization failed. Falling back to Canvas 3D.\",error);\r\n            try{\r\n                if(renderer)renderer.dispose();\r\n            }catch(disposeError){}\r\n            renderer=null;\r\n            scene=null;\r\n            camera=null;\r\n            controls=null;\r\n            containerGroup=null;\r\n            cargoGroup=null;\r\n            sectionGroup=null;\r\n        }\r\n    }\r\n\r\n    initCanvas3DFallback();\r\n}\r\n\r\nfunction observeCanvasResize(){\r\n    const canvas=byId(\"nscLpCanvas\");\r\n    if(!canvas)return;\r\n    if(fallbackResizeObserver){\r\n        try{fallbackResizeObserver.disconnect();}catch(error){}\r\n        fallbackResizeObserver=null;\r\n    }\r\n    if(typeof ResizeObserver!==\"undefined\"){\r\n        fallbackResizeObserver=new ResizeObserver(function(){\r\n            resize3D();\r\n            if(renderMode===\"canvas3d\")renderFallbackScene();\r\n        });\r\n        fallbackResizeObserver.observe(canvas.parentElement||canvas);\r\n    }\r\n}\r\n\r\nfunction initCanvas3DFallback(){\r\n    const canvas=byId(\"nscLpCanvas\");\r\n    if(!canvas)return;\r\n\r\n    renderMode=\"canvas3d\";\r\n    fallbackCanvas=canvas;\r\n    fallbackCtx=canvas.getContext(\"2d\",{alpha:false});\r\n    if(!fallbackCtx){\r\n        renderMode=\"none\";\r\n        const note=canvas.parentElement&&canvas.parentElement.querySelector(\".nsc-lp-canvas-note\");\r\n        if(note)note.textContent=\"3D visualization is unavailable in this browser. Please enable HTML5 Canvas support.\";\r\n        return;\r\n    }\r\n\r\n    fallbackCamera.yaw=-0.65;\r\n    fallbackCamera.pitch=0.38;\r\n    fallbackCamera.zoom=1;\r\n    fallbackCamera.panX=0;\r\n    fallbackCamera.panY=0;\r\n\r\n    bindFallbackControls(canvas);\r\n    resize3D();\r\n    observeCanvasResize();\r\n\r\n    requestAnimationFrame(function(){\r\n        resize3D();\r\n        renderFallbackScene();\r\n    });\r\n    window.setTimeout(function(){\r\n        resize3D();\r\n        renderFallbackScene();\r\n    },120);\r\n}\r\n\r\nfunction bindFallbackControls(canvas){\r\n    if(canvas.dataset.nscFallbackBound===\"true\")return;\r\n    canvas.dataset.nscFallbackBound=\"true\";\r\n\r\n    let activePointer=null;\r\n    let lastX=0;\r\n    let lastY=0;\r\n    let dragging=false;\r\n\r\n    canvas.addEventListener(\"pointerdown\",function(event){\r\n        activePointer=event.pointerId;\r\n        lastX=event.clientX;\r\n        lastY=event.clientY;\r\n        dragging=true;\r\n        canvas.setPointerCapture(event.pointerId);\r\n        event.preventDefault();\r\n    });\r\n\r\n    canvas.addEventListener(\"pointermove\",function(event){\r\n        if(!dragging || activePointer!==event.pointerId)return;\r\n        const dx=event.clientX-lastX;\r\n        const dy=event.clientY-lastY;\r\n        lastX=event.clientX;\r\n        lastY=event.clientY;\r\n\r\n        if(event.buttons===2 || event.shiftKey){\r\n            fallbackCamera.panX+=dx*0.012\/fallbackCamera.zoom;\r\n            fallbackCamera.panY-=dy*0.012\/fallbackCamera.zoom;\r\n        }else{\r\n            fallbackCamera.yaw+=dx*0.012;\r\n            fallbackCamera.pitch=clamp(fallbackCamera.pitch-dy*0.008,-0.15,1.15);\r\n        }\r\n        renderFallbackScene();\r\n        event.preventDefault();\r\n    });\r\n\r\n    const endPointer=function(event){\r\n        if(activePointer!==event.pointerId)return;\r\n        dragging=false;\r\n        try{canvas.releasePointerCapture(event.pointerId);}catch(error){}\r\n        activePointer=null;\r\n    };\r\n    canvas.addEventListener(\"pointerup\",endPointer);\r\n    canvas.addEventListener(\"pointercancel\",endPointer);\r\n    canvas.addEventListener(\"contextmenu\",function(event){event.preventDefault();});\r\n\r\n    canvas.addEventListener(\"wheel\",function(event){\r\n        const factor=Math.exp(-event.deltaY*0.0012);\r\n        fallbackCamera.zoom=clamp(fallbackCamera.zoom*factor,.55,3.5);\r\n        renderFallbackScene();\r\n        event.preventDefault();\r\n    },{passive:false});\r\n}\r\n\r\nfunction resize3D(){\r\n    const canvas=byId(\"nscLpCanvas\");\r\n    if(!canvas)return;\r\n    const size=getCanvasSize(canvas);\r\n\r\n    if(renderMode===\"three\" && renderer && camera){\r\n        const w=size.w;\r\n        const h=size.h;\r\n        renderer.setSize(w,h,false);\r\n        const e=currentEquipment();\r\n        const maxDim=Math.max(e.length,e.height,e.width)\/1000;\r\n        const halfHeight=Math.max(maxDim*.60,.5);\r\n        const halfWidth=halfHeight*(w\/h);\r\n        camera.left=-halfWidth;\r\n        camera.right=halfWidth;\r\n        camera.top=halfHeight;\r\n        camera.bottom=-halfHeight;\r\n        camera.updateProjectionMatrix();\r\n        return;\r\n    }\r\n\r\n    if(renderMode===\"canvas3d\" && fallbackCanvas){\r\n        const dpr=Math.min(window.devicePixelRatio||1,2);\r\n        const targetW=Math.round(size.w*dpr);\r\n        const targetH=Math.round(size.h*dpr);\r\n        if(fallbackCanvas.width!==targetW || fallbackCanvas.height!==targetH){\r\n            fallbackCanvas.width=targetW;\r\n            fallbackCanvas.height=targetH;\r\n        }\r\n        fallbackCanvas.style.width=size.w+\"px\";\r\n        fallbackCanvas.style.height=size.h+\"px\";\r\n        if(fallbackCtx){\r\n            fallbackCtx.setTransform(dpr,0,0,dpr,0,0);\r\n            fallbackCtx.imageSmoothingEnabled=true;\r\n        }\r\n    }\r\n}\r\n\r\nfunction animate3D(){\r\n    requestAnimationFrame(animate3D);\r\n    if(renderMode!==\"three\")return;\r\n    if(controls)controls.update();\r\n    if(renderer&&scene&&camera)renderer.render(scene,camera);\r\n}\r\n\r\nfunction stopFallbackAutoRotate(){\r\n    fallbackAutoRotate=false;\r\n    if(fallbackAnimationFrame){\r\n        window.cancelAnimationFrame(fallbackAnimationFrame);\r\n        fallbackAnimationFrame=null;\r\n    }\r\n}\r\n\r\nfunction fallbackAutoRotateLoop(){\r\n    if(!fallbackAutoRotate){\r\n        fallbackAnimationFrame=null;\r\n        return;\r\n    }\r\n    fallbackCamera.yaw+=.006;\r\n    renderFallbackScene();\r\n    fallbackAnimationFrame=window.requestAnimationFrame(fallbackAutoRotateLoop);\r\n}\r\n\r\nfunction startFallbackAutoRotate(){\r\n    if(fallbackAutoRotate)return;\r\n    fallbackAutoRotate=true;\r\n    fallbackAnimationFrame=window.requestAnimationFrame(fallbackAutoRotateLoop);\r\n}\r\n\r\nfunction setViewControlState(active){\r\n    const button=byId(\"nscView360\");\r\n    if(button){\r\n        button.classList.toggle(\"active\",!!active);\r\n        button.setAttribute(\"aria-pressed\",active?\"true\":\"false\");\r\n    }\r\n}\r\n\r\nfunction reset3DView(){\r\n    stopFallbackAutoRotate();\r\n\r\n    if(renderMode===\"three\" && camera && controls){\r\n        const e=currentEquipment();\r\n        const L=e.length\/1000;\r\n        const W=e.width\/1000;\r\n        const H=boundsFor(e).H\/1000;\r\n        const maxDim=Math.max(L,W,H);\r\n        camera.position.set(-maxDim*1.05,maxDim*.8,maxDim*1.1);\r\n        controls.target.set(0,H*.42,0);\r\n        camera.zoom=1;\r\n        controls.autoRotate=false;\r\n        controls.update();\r\n        resize3D();\r\n        if(renderer&&scene)renderer.render(scene,camera);\r\n    }else{\r\n        fallbackCamera.yaw=-.65;\r\n        fallbackCamera.pitch=.38;\r\n        fallbackCamera.zoom=1;\r\n        fallbackCamera.panX=0;\r\n        fallbackCamera.panY=0;\r\n        renderFallbackScene();\r\n    }\r\n\r\n    setViewControlState(false);\r\n}\r\n\r\nfunction toggle360View(){\r\n    const button=byId(\"nscView360\");\r\n    const active=button ? button.getAttribute(\"aria-pressed\")===\"true\" : false;\r\n    if(active){\r\n        stopFallbackAutoRotate();\r\n        if(renderMode===\"three\" && controls)controls.autoRotate=false;\r\n        setViewControlState(false);\r\n        return;\r\n    }\r\n\r\n    setViewControlState(true);\r\n    if(renderMode===\"three\" && controls){\r\n        controls.autoRotate=true;\r\n        controls.autoRotateSpeed=.75;\r\n    }else if(renderMode===\"canvas3d\"){\r\n        startFallbackAutoRotate();\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   STANDALONE CANVAS 3D FALLBACK\r\n   ---------------------------------------------------------\r\n   This renderer does not depend on Three.js. It keeps the\r\n   3D planner working inside Elementor even when third-party\r\n   scripts are blocked, delayed or unavailable.\r\n========================================================= *\/\r\nfunction hexRgb(hex){\r\n    const n=parseInt(String(hex||\"\").replace(\"#\",\"\"),16);\r\n    if(!Number.isFinite(n))return {r:11,g:79,b:138};\r\n    return {r:(n>>16)&255,g:(n>>8)&255,b:n&255};\r\n}\r\nfunction shadeHex(hex,amount){\r\n    const c=hexRgb(hex);\r\n    const r=clamp(Math.round(c.r+amount),0,255);\r\n    const g=clamp(Math.round(c.g+amount),0,255);\r\n    const b=clamp(Math.round(c.b+amount),0,255);\r\n    return `rgb(${r},${g},${b})`;\r\n}\r\nfunction fallbackProject(pt,sceneInfo){\r\n    const cy=Math.cos(fallbackCamera.yaw);\r\n    const sy=Math.sin(fallbackCamera.yaw);\r\n    const cp=Math.cos(fallbackCamera.pitch);\r\n    const sp=Math.sin(fallbackCamera.pitch);\r\n    const x1=pt.x*cy-pt.z*sy;\r\n    const z1=pt.x*sy+pt.z*cy;\r\n    const y1=pt.y*cp-z1*sp;\r\n    const z2=pt.y*sp+z1*cp;\r\n\r\n    \/* CAD-style orthographic projection prevents perspective convergence\r\n       from making the container look skewed at extreme 360\u00b0 angles. *\/\r\n    return {\r\n        x:sceneInfo.cx+(x1+fallbackCamera.panX)*sceneInfo.scale,\r\n        y:sceneInfo.cy-(y1+fallbackCamera.panY)*sceneInfo.scale,\r\n        depth:z2,\r\n        factor:1\r\n    };\r\n}\r\nfunction drawPoly(ctx,points,fill,stroke,alpha=1){\r\n    if(!points.length)return;\r\n    ctx.save();\r\n    ctx.globalAlpha=alpha;\r\n    ctx.beginPath();\r\n    ctx.moveTo(points[0].x,points[0].y);\r\n    for(let i=1;i<points.length;i++)ctx.lineTo(points[i].x,points[i].y);\r\n    ctx.closePath();\r\n    ctx.fillStyle=fill;\r\n    ctx.fill();\r\n    if(stroke){\r\n        ctx.strokeStyle=stroke;\r\n        ctx.lineWidth=1;\r\n        ctx.stroke();\r\n    }\r\n    ctx.restore();\r\n}\r\nfunction fallbackBoxFaces(box,sceneInfo,baseColor,alpha=1){\r\n    const x=box.x,y=box.y,z=box.z;\r\n    const l=box.l,w=box.w,h=box.h;\r\n    const v=[\r\n        {x:x,y:y,z:z},{x:x+l,y:y,z:z},\r\n        {x:x+l,y:y,z:z+w},{x:x,y:y,z:z+w},\r\n        {x:x,y:y+h,z:z},{x:x+l,y:y+h,z:z},\r\n        {x:x+l,y:y+h,z:z+w},{x:x,y:y+h,z:z+w}\r\n    ];\r\n    const p=v.map(pt=>fallbackProject(pt,sceneInfo));\r\n    const faces=[\r\n        {idx:[4,5,6,7],color:shadeHex(baseColor,28)},\r\n        {idx:[0,1,5,4],color:shadeHex(baseColor,8)},\r\n        {idx:[1,2,6,5],color:shadeHex(baseColor,-8)},\r\n        {idx:[2,3,7,6],color:shadeHex(baseColor,-18)},\r\n        {idx:[3,0,4,7],color:shadeHex(baseColor,-3)},\r\n        {idx:[0,3,2,1],color:shadeHex(baseColor,-24)}\r\n    ];\r\n    return faces.map(face=>({\r\n        points:face.idx.map(i=>p[i]),\r\n        color:face.color,\r\n        depth:face.idx.reduce((s,i)=>s+p[i].depth,0)\/face.idx.length,\r\n        alpha:alpha\r\n    }));\r\n}\r\nfunction drawLine3D(ctx,pts,sceneInfo,color,width=1,alpha=1,dash=[]){\r\n    const p=pts.map(pt=>fallbackProject(pt,sceneInfo));\r\n    ctx.save();\r\n    ctx.globalAlpha=alpha;\r\n    ctx.strokeStyle=color;\r\n    ctx.lineWidth=width;\r\n    if(dash.length)ctx.setLineDash(dash);\r\n    ctx.beginPath();\r\n    ctx.moveTo(p[0].x,p[0].y);\r\n    for(let i=1;i<p.length;i++)ctx.lineTo(p[i].x,p[i].y);\r\n    ctx.stroke();\r\n    ctx.restore();\r\n}\r\nfunction drawContainerFallback(ctx,e,sceneInfo){\r\n    if (typeof specialDrawContainerFallback === \"function\" && !isDryEquipment(e)) {\r\n        specialDrawContainerFallback(ctx,e,sceneInfo);\r\n        return;\r\n    }\r\n    const L=e.length\/1000,W=e.width\/1000,H=boundsFor(e).H\/1000;\r\n    const frameColor=\"#536273\";\r\n    const softColor=\"#95a4b2\";\r\n\r\n    drawPoly(ctx,\r\n        [0,1,2,3].map((i)=>fallbackProject([\r\n            {x:-L\/2,y:0,z:-W\/2},\r\n            {x:L\/2,y:0,z:-W\/2},\r\n            {x:L\/2,y:0,z:W\/2},\r\n            {x:-L\/2,y:0,z:W\/2}\r\n        ][i],sceneInfo)),\r\n        \"rgba(47,128,192,.92)\",\r\n        \"rgba(201,162,39,.72)\"\r\n    );\r\n\r\n    const corners=[\r\n        [-L\/2,0,-W\/2],[L\/2,0,-W\/2],[L\/2,0,W\/2],[-L\/2,0,W\/2],\r\n        [-L\/2,H,-W\/2],[L\/2,H,-W\/2],[L\/2,H,W\/2],[-L\/2,H,W\/2]\r\n    ];\r\n    const edges=[[0,1],[1,2],[2,3],[3,0],[4,5],[5,6],[6,7],[7,4],[0,4],[1,5],[2,6],[3,7]];\r\n    edges.forEach(pair=>drawLine3D(ctx,pair.map(i=>({x:corners[i][0],y:corners[i][1],z:corners[i][2]})),sceneInfo,frameColor,1.2,.78));\r\n\r\n    \/* Intentionally no large translucent wall polygons. Transparency can\r\n       reorder draw layers and make the container appear visually warped\r\n       during rotation. The fixed geometric frame remains stable instead. *\/\r\n\r\n    \/* Door \/ opening side = orange, +X end. Back \/ inside = brand blue, -X end. *\/\r\n    const doorW=Math.min(e.doorWidth||e.width,e.width)\/1000;\r\n    const doorH=Math.min(e.doorHeight||e.heightNoBars||e.height,H*1000)\/1000;\r\n    const doorX=L\/2+.002;\r\n    const insideX=-L\/2-.002;\r\n    const markerBackPts=[\r\n        {x:insideX,y:0,z:-W*.47},\r\n        {x:insideX,y:H*.96,z:-W*.47},\r\n        {x:insideX,y:H*.96,z:W*.47},\r\n        {x:insideX,y:0,z:W*.47}\r\n    ].map(pt=>fallbackProject(pt,sceneInfo));\r\n    drawPoly(ctx,markerBackPts,\"rgba(11,79,138,.055)\",\"rgba(11,79,138,.82)\",1);\r\n\r\n    const doorPts=[\r\n        {x:doorX,y:0,z:-doorW\/2},\r\n        {x:doorX,y:doorH,z:-doorW\/2},\r\n        {x:doorX,y:doorH,z:doorW\/2},\r\n        {x:doorX,y:0,z:doorW\/2}\r\n    ].map(pt=>fallbackProject(pt,sceneInfo));\r\n    drawPoly(ctx,doorPts,\"rgba(245,158,11,.055)\",\"rgba(245,158,11,.95)\",1);\r\n\r\n    if(e.type.startsWith(\"ot\")){\r\n        const roofL=(e.roofLength||e.length)\/1000;\r\n        const roofW=(e.roofWidth||e.width)\/1000;\r\n        const y=H+.015;\r\n        drawLine3D(ctx,[\r\n            {x:-roofL\/2,y:y,z:-roofW\/2},\r\n            {x:roofL\/2,y:y,z:-roofW\/2},\r\n            {x:roofL\/2,y:y,z:roofW\/2},\r\n            {x:-roofL\/2,y:y,z:roofW\/2},\r\n            {x:-roofL\/2,y:y,z:-roofW\/2}\r\n        ],sceneInfo,\"#c9a227\",1.3,.78,[5,4]);\r\n    }\r\n\r\n    if(byId(\"nscPackMode\") && byId(\"nscPackMode\").value===\"section\" && !e.type.startsWith(\"fr\")){\r\n        const r=sectionRanges(e);\r\n        for(let i=1;i<4;i++){\r\n            const x=(r[i]-e.length\/2)\/1000;\r\n            drawLine3D(ctx,[\r\n                {x:x,y:.01,z:-W\/2},{x:x,y:.01,z:W\/2}\r\n            ],sceneInfo,\"#c9a227\",1,.7,[3,4]);\r\n        }\r\n    }\r\n\r\n    \/* Corner \/ opening guides for flat racks. *\/\r\n    if(e.type.startsWith(\"fr\")){\r\n        const z=e.minWidth?e.minWidth\/1000\/2:W\/2;\r\n        drawLine3D(ctx,[\r\n            {x:-L\/2,y:0,z:-z},{x:L\/2,y:0,z:-z}\r\n        ],sceneInfo,softColor,1,.65,[3,3]);\r\n        drawLine3D(ctx,[\r\n            {x:-L\/2,y:0,z:z},{x:L\/2,y:0,z:z}\r\n        ],sceneInfo,softColor,1,.65,[3,3]);\r\n    }\r\n}\r\nfunction renderFallbackScene(plan){\r\n    if(renderMode!==\"canvas3d\"||!fallbackCtx||!fallbackCanvas)return;\r\n\r\n    \/* Preserve the last generated loading plan on redraw\/resize.\r\n       Passing null explicitly is used only when the planner should clear it. *\/\r\n    const activePlan = (plan === undefined) ? currentPlan : plan;\r\n\r\n    resize3D();\r\n    const size=getCanvasSize(fallbackCanvas);\r\n    const ctx=fallbackCtx;\r\n    ctx.clearRect(0,0,size.w,size.h);\r\n    const g=ctx.createLinearGradient(0,0,0,size.h);\r\n    g.addColorStop(0,\"#eef4f8\");\r\n    g.addColorStop(1,\"#dfe8ef\");\r\n    ctx.fillStyle=g;\r\n    ctx.fillRect(0,0,size.w,size.h);\r\n\r\n    const e=(activePlan&&activePlan.equipment)||currentEquipment();\r\n    const L=e.length\/1000;\r\n    const W=e.width\/1000;\r\n    const H=boundsFor(e).H\/1000;\r\n    const maxDim=Math.max(L,W,H);\r\n    const scale=Math.min(size.w\/(maxDim*1.42),size.h\/(maxDim*1.22))*fallbackCamera.zoom;\r\n    const sceneInfo={\r\n        cx:size.w\/2,\r\n        cy:size.h*.64,\r\n        scale:scale,\r\n        maxDim:maxDim\r\n    };\r\n\r\n    \/* Bright NS Continent loading-yard floor around the container.\r\n       The yard is intentionally below the container floor so the platform\r\n       elevation is visible in the fallback renderer as well. *\/\r\n    const yardL=L*1.72;\r\n    const yardW=W*1.72;\r\n    const yardY=-NSC_GROUND_CLEARANCE;\r\n    const yardPoints=[\r\n        {x:-yardL\/2,y:yardY,z:-yardW\/2},\r\n        {x:yardL\/2,y:yardY,z:-yardW\/2},\r\n        {x:yardL\/2,y:yardY,z:yardW\/2},\r\n        {x:-yardL\/2,y:yardY,z:yardW\/2}\r\n    ].map(pt=>fallbackProject(pt,sceneInfo));\r\n    drawPoly(ctx,yardPoints,\"#69c6ee\",\"rgba(1,45,90,.48)\",.98);\r\n\r\n    \/* Subtle loading-yard grid to make the surrounding floor more visible. *\/\r\n    for(let i=1;i<8;i++){\r\n        const gx=-yardL\/2+(yardL*i\/8);\r\n        drawLine3D(ctx,[\r\n            {x:gx,y:yardY+.01,z:-yardW\/2},\r\n            {x:gx,y:yardY+.01,z:yardW\/2}\r\n        ],sceneInfo,\"rgba(11,79,138,.20)\",.7,.55,[3,4]);\r\n    }\r\n    for(let i=1;i<8;i++){\r\n        const gz=-yardW\/2+(yardW*i\/8);\r\n        drawLine3D(ctx,[\r\n            {x:-yardL\/2,y:yardY+.01,z:gz},\r\n            {x:yardL\/2,y:yardY+.01,z:gz}\r\n        ],sceneInfo,\"rgba(255,255,255,.30)\",.7,.55,[3,4]);\r\n    }\r\n\r\n    \/* Flat Rack support legs make the ground-to-platform separation obvious. *\/\r\n    if(e.type.startsWith(\"fr\")){\r\n        const supportColor=\"#475569\";\r\n        [[-L*.34,-W*.38],[L*.34,-W*.38],[-L*.34,W*.38],[L*.34,W*.38]].forEach(function(pair){\r\n            drawLine3D(ctx,[\r\n                {x:pair[0],y:yardY+.03,z:pair[1]},\r\n                {x:pair[0],y:0,z:pair[1]}\r\n            ],sceneInfo,supportColor,3,.88);\r\n        });\r\n    }\r\n\r\n    drawContainerFallback(ctx,e,sceneInfo);\r\n\r\n    const faces=[];\r\n    const cargo=(activePlan&&Array.isArray(activePlan.placed))?activePlan.placed:[];\r\n    cargo.forEach(function(p){\r\n        fallbackBoxFaces({\r\n            x:(p.x-e.length\/2)\/1000,\r\n            y:p.y\/1000,\r\n            z:(p.z-e.width\/2)\/1000,\r\n            l:p.l\/1000,\r\n            w:p.w\/1000,\r\n            h:p.h\/1000\r\n        },sceneInfo,p.color,.88).forEach(f=>faces.push(f));\r\n    });\r\n    faces.sort((a,b)=>a.depth-b.depth);\r\n    faces.forEach(f=>drawPoly(ctx,f.points,f.color,\"rgba(23,43,58,.58)\",f.alpha));\r\n\r\n    \/* Simple center line \/ horizon marker. *\/\r\n    drawLine3D(ctx,[\r\n        {x:-L\/2,y:0.002,z:0},{x:L\/2,y:0.002,z:0}\r\n    ],sceneInfo,\"rgba(1,45,90,.34)\",1,.55,[4,4]);\r\n\r\n    const backLabel=fallbackProject({x:-L\/2-.02,y:H*.99,z:0},sceneInfo);\r\n    const doorLabel=fallbackProject({x:L\/2+.02,y:H*.99,z:0},sceneInfo);\r\n    ctx.save();\r\n    ctx.font=\"900 9px Arial, Helvetica, sans-serif\";\r\n    ctx.fillStyle=\"#0b4f8a\";\r\n    ctx.fillText(\"BACK \/ INSIDE\",backLabel.x-34,backLabel.y-5);\r\n    ctx.fillStyle=\"#b96d05\";\r\n    ctx.fillText(\"DOOR \/ OPENING\",doorLabel.x-42,doorLabel.y-5);\r\n    ctx.restore();\r\n\r\n    ctx.save();\r\n    ctx.fillStyle=\"rgba(1,45,90,.76)\";\r\n    ctx.font=\"700 11px Arial, Helvetica, sans-serif\";\r\n    ctx.fillText(`${e.label} \u00b7 ${renderMode===\"three\"?\"WebGL\":\"Canvas 3D\"}`,14,20);\r\n    ctx.fillStyle=\"rgba(70,88,105,.72)\";\r\n    ctx.font=\"10px Arial, Helvetica, sans-serif\";\r\n    ctx.fillText(\"Drag = rotate 360\u00b0 \u00b7 Right-drag \/ Shift-drag = pan \u00b7 Wheel = zoom\",14,36);\r\n    if(e.type.startsWith(\"fr\")){\r\n        ctx.fillStyle=\"rgba(70,88,105,.72)\";\r\n        ctx.fillText(\"Flat Rack platform elevated above loading yard for clear visual separation\",14,52);\r\n    }\r\n    ctx.restore();\r\n}\r\n\r\nfunction clearGroup(group){\r\n    if(!group)return;\r\n    while(group.children.length){\r\n        const obj=group.children[group.children.length-1];\r\n        group.remove(obj);\r\n        obj.traverse(function(node){\r\n            if(node.geometry)node.geometry.dispose();\r\n            if(node.material){\r\n                if(Array.isArray(node.material))node.material.forEach(m=>m.dispose());\r\n                else node.material.dispose();\r\n            }\r\n        });\r\n    }\r\n}\r\n\r\nfunction destroySceneObjects(){\r\n    clearGroup(cargoGroup);\r\n    clearGroup(sectionGroup);\r\n    if (typeof specialRenderContainer === \"function\" && !isDryEquipment(currentEquipment())) {\r\n        specialRenderContainer();\r\n    } else {\r\n        renderContainer();\r\n    }\r\n}\r\n\r\nfunction renderContainer(){\r\n    if (typeof specialRenderContainer === \"function\" && !isDryEquipment(currentEquipment())) {\r\n        specialRenderContainer();\r\n        return;\r\n    }\r\n    if(!scene||!containerGroup)return;\r\n    clearGroup(containerGroup);\r\n    const e=currentEquipment();\r\n\r\n    const L=e.length\/1000;\r\n    const W=e.width\/1000;\r\n    const H=e.height\/1000;\r\n\r\n    const floorMat=new THREE.MeshStandardMaterial({\r\n        color:0x2f80c0,\r\n        roughness:.76,\r\n        metalness:.06\r\n    });\r\n\r\n    const floor=new THREE.Mesh(\r\n        new THREE.BoxGeometry(L,.08,W),\r\n        floorMat\r\n    );\r\n    floor.position.y=.04;\r\n    containerGroup.add(floor);\r\n\r\n    const frameMat=new THREE.MeshStandardMaterial({\r\n        color:0x687684,\r\n        roughness:.72,\r\n        metalness:.18\r\n    });\r\n\r\n    const goldMat=new THREE.MeshStandardMaterial({\r\n        color:0xc9a227,\r\n        roughness:.65,\r\n        metalness:.14\r\n    });\r\n\r\n    function addBeam(x,y,z,bx,by,bz,material){\r\n        const beam=new THREE.Mesh(\r\n            new THREE.BoxGeometry(bx,by,bz),\r\n            material||frameMat\r\n        );\r\n        beam.position.set(x,y,z);\r\n        containerGroup.add(beam);\r\n    }\r\n\r\n    const t=.075;\r\n    const post=.105;\r\n\r\n    [[-L\/2,-W\/2],[L\/2,-W\/2],[-L\/2,W\/2],[L\/2,W\/2]].forEach(function(pair){\r\n        addBeam(pair[0],H\/2,pair[1],post,H,post,frameMat);\r\n    });\r\n\r\n    \/* Bottom perimeter. *\/\r\n    addBeam(0,t\/2,-W\/2,L,t,t,frameMat);\r\n    addBeam(0,t\/2,W\/2,L,t,t,frameMat);\r\n    addBeam(-L\/2,t\/2,0,t,t,W,frameMat);\r\n    addBeam(L\/2,t\/2,0,t,t,W,frameMat);\r\n\r\n    if(!e.type.startsWith(\"fr\")){\r\n        \/* Top structural perimeter. Dry container remains visibly closed by\r\n           its outline; Open Top has the same rim but no roof surface. *\/\r\n        addBeam(0,H-t\/2,-W\/2,L,t,t,frameMat);\r\n        addBeam(0,H-t\/2,W\/2,L,t,t,frameMat);\r\n        addBeam(-L\/2,H-t\/2,0,t,t,W,frameMat);\r\n        addBeam(L\/2,H-t\/2,0,t,t,W,frameMat);\r\n    }else{\r\n        \/* Flat Rack: end frames without side walls. *\/\r\n        const endT=.09;\r\n        addBeam(-L\/2,H\/2,0,endT,H,W,frameMat);\r\n        addBeam(L\/2,H\/2,0,endT,H,W,frameMat);\r\n        addBeam(0,H*.74,-W\/2,L*.12,.08,.08,goldMat);\r\n        addBeam(0,H*.74,W\/2,L*.12,.08,.08,goldMat);\r\n    }\r\n\r\n    \/* Door \/ opening side visual marker. Door is the +X end of the model. *\/\r\n    const doorColor=0xf59e0b;\r\n    const doorFrameMat=new THREE.MeshStandardMaterial({\r\n        color:doorColor,\r\n        roughness:.62,\r\n        metalness:.08\r\n    });\r\n    const doorW=Math.min(e.doorWidth||e.width,e.width);\r\n    const doorH=Math.min(e.doorHeight||e.heightNoBars||e.height,H*1000)\/1000;\r\n    addBeam(L\/2+.012,doorH\/2,0,.024,.09,doorW\/1000,doorFrameMat);\r\n    addBeam(L\/2+.014,doorH-.045,-(doorW\/1000)\/2,.024,.09,.024,doorFrameMat);\r\n    addBeam(L\/2+.014,doorH-.045,(doorW\/1000)\/2,.024,.09,.024,doorFrameMat);\r\n\r\n    const doorPanel=new THREE.Mesh(\r\n        new THREE.BoxGeometry(.015,doorH,Math.max(.05,(doorW\/1000)-.06)),\r\n        new THREE.MeshBasicMaterial({\r\n            color:doorColor,\r\n            transparent:true,\r\n            opacity:.055,\r\n            depthWrite:false,\r\n            side:THREE.DoubleSide\r\n        })\r\n    );\r\n    doorPanel.position.set(L\/2+.004,doorH\/2,0);\r\n    containerGroup.add(doorPanel);\r\n\r\n    \/* Back \/ inside marker uses a restrained brand-blue outline. *\/\r\n    const insideMat=new THREE.MeshStandardMaterial({\r\n        color:0x0b4f8a,\r\n        roughness:.78,\r\n        metalness:.05\r\n    });\r\n    addBeam(-L\/2-.010,H\/2,0,.020,H,.045,insideMat);\r\n\r\n    if(e.type.startsWith(\"ot\")){\r\n        addRoofOpeningFrame(containerGroup,e);\r\n    }\r\n\r\n    const edgeGeo=new THREE.EdgesGeometry(new THREE.BoxGeometry(L,H,W));\r\n    const edges=new THREE.LineSegments(\r\n        edgeGeo,\r\n        new THREE.LineBasicMaterial({\r\n            color:0x41505e,\r\n            transparent:true,\r\n            opacity:.62\r\n        })\r\n    );\r\n    edges.position.y=H\/2;\r\n    containerGroup.add(edges);\r\n}\r\n\r\nfunction addLineEdges(group,L,H,W,opacity){\r\n    const mat=new THREE.LineBasicMaterial({color:0x475569,transparent:true,opacity:opacity});\r\n    const pts=[];\r\n    const y0=0,y1=H;\r\n    const x1=-L\/2,x2=L\/2,z1=-W\/2,z2=W\/2;\r\n    const add=(a,b)=>{pts.push(a.x,a.y,a.z,b.x,b.y,b.z);};\r\n    add(new THREE.Vector3(x1,y0,z1),new THREE.Vector3(x2,y0,z1));\r\n    add(new THREE.Vector3(x1,y0,z2),new THREE.Vector3(x2,y0,z2));\r\n    add(new THREE.Vector3(x1,y0,z1),new THREE.Vector3(x1,y0,z2));\r\n    add(new THREE.Vector3(x2,y0,z1),new THREE.Vector3(x2,y0,z2));\r\n    add(new THREE.Vector3(x1,y1,z1),new THREE.Vector3(x2,y1,z1));\r\n    add(new THREE.Vector3(x1,y1,z2),new THREE.Vector3(x2,y1,z2));\r\n    const geometry=new THREE.BufferGeometry();\r\n    geometry.setAttribute(\"position\",new THREE.Float32BufferAttribute(pts,3));\r\n    group.add(new THREE.LineSegments(geometry,mat));\r\n}\r\n\r\nfunction addRoofOpeningFrame(group,e){\r\n    const L=e.length\/1000,W=e.width\/1000,H=e.height\/1000;\r\n    const mat=new THREE.LineBasicMaterial({color:0xc9a227,transparent:true,opacity:.75});\r\n    const geo=new THREE.BufferGeometry();\r\n    const p=[];\r\n    const x=e.roofLength\/1000\/2;\r\n    const z=e.roofWidth\/1000\/2;\r\n    const y=H+.02;\r\n    const pts=[\r\n        [-x,y,-z,x,y,-z],\r\n        [x,y,-z,x,y,z],\r\n        [x,y,z,-x,y,z],\r\n        [-x,y,z,-x,y,-z]\r\n    ];\r\n    pts.forEach(v=>p.push(...v));\r\n    geo.setAttribute(\"position\",new THREE.Float32BufferAttribute(p,3));\r\n    group.add(new THREE.LineSegments(geo,mat));\r\n}\r\n\r\n\/* =========================================================\r\n   PACKING ENGINE\r\n\r\n   Heuristic principles:\r\n   1. Expand quantity into individual pieces.\r\n   2. Heavy cargo first.\r\n   3. Section mode divides length into 4 longitudinal zones.\r\n   4. Candidate rotations are checked.\r\n   5. Placement is from low Z\/Y toward the next open support.\r\n   6. Non-overlap and equipment envelope are hard constraints.\r\n========================================================= *\/\r\nfunction expandCargo(){\r\n    const pieces=[];\r\n    cargoRows.forEach(function(row){\r\n        for(let i=0;i<row.qty;i++){\r\n            pieces.push({\r\n                uid:`${row.id}-${i+1}`,\r\n                rowId:row.id,\r\n                shipper:row.shipper,\r\n                description:row.description,\r\n                l:row.length,\r\n                w:row.width,\r\n                h:row.height,\r\n                weight:row.weight,\r\n                maxStack:row.maxStack,\r\n                rotation:row.rotation,\r\n                stackable:row.stackable!==false,\r\n                rollable:row.rollable===true,\r\n                color:row.color,\r\n                index:i+1,\r\n                placed:false\r\n            });\r\n        }\r\n    });\r\n\r\n    pieces.sort(function(a,b){\r\n        const wa=a.weight*b.l*b.w*b.h;\r\n        const wb=b.weight*a.l*a.w*a.h;\r\n        if(b.weight!==a.weight)return b.weight-a.weight;\r\n        return wb-wa;\r\n    });\r\n    return pieces;\r\n}\r\n\r\nfunction rotationsFor(piece){\r\n    const l=piece.l,w=piece.w,h=piece.h;\r\n    const all=[\r\n        [l,w,h],\r\n        [l,h,w],\r\n        [w,l,h],\r\n        [w,h,l],\r\n        [h,l,w],\r\n        [h,w,l]\r\n    ];\r\n    let unique=[];\r\n    const seen=new Set();\r\n    all.forEach(function(r){\r\n        const key=r.join(\"x\");\r\n        if(seen.has(key))return;\r\n        seen.add(key);\r\n        unique.push(r);\r\n    });\r\n    if(piece.rotation===\"no-rotate\")unique=[[l,w,h]];\r\n    if(piece.rotation===\"floor\"){\r\n        unique=unique.filter(r=>r[2]===h);\r\n    }\r\n    return unique;\r\n}\r\n\r\nfunction boundsFor(e){\r\n    return {\r\n        L:e.length,\r\n        W:e.width,\r\n        H:e.type.startsWith(\"fr\")?e.height:(e.heightNoBars||e.height),\r\n        strictHeight:e.height,\r\n        doorWidth:e.doorWidth||null,\r\n        doorHeight:e.doorHeight||null\r\n    };\r\n}\r\n\r\nfunction overlap(a,b,gap){\r\n    return !(\r\n        a.x+a.l <= b.x+gap ||\r\n        b.x+b.l <= a.x+gap ||\r\n        a.y+a.h <= b.y+gap ||\r\n        b.y+b.h <= a.y+gap ||\r\n        a.z+a.w <= b.z+gap ||\r\n        b.z+b.w <= a.z+gap\r\n    );\r\n}\r\n\r\nfunction supportedBy(placement,placed,gap){\r\n    if(placement.y<=gap)return true;\r\n    const supportTop=placement.y;\r\n    const footprintX1=placement.x+gap;\r\n    const footprintX2=placement.x+placement.l-gap;\r\n    const footprintZ1=placement.z+gap;\r\n    const footprintZ2=placement.z+placement.w-gap;\r\n    for(const b of placed){\r\n        if(b.stackable===false)continue;\r\n        const top=b.y+b.h;\r\n        if(Math.abs(top-supportTop)>Math.max(gap,2))continue;\r\n        const xOverlap=Math.min(placement.x+placement.l,b.x+b.l)-Math.max(placement.x,b.x);\r\n        const zOverlap=Math.min(placement.z+placement.w,b.z+b.w)-Math.max(placement.z,b.z);\r\n        if(xOverlap<=0||zOverlap<=0)continue;\r\n        \/* Require broad footprint support instead of a tiny touching edge. *\/\r\n        const ox1=Math.max(placement.x,b.x);\r\n        const ox2=Math.min(placement.x+placement.l,b.x+b.l);\r\n        const oz1=Math.max(placement.z,b.z);\r\n        const oz2=Math.min(placement.z+placement.w,b.z+b.w);\r\n        if(ox1<=footprintX1&&ox2>=footprintX2&&oz1<=footprintZ1&&oz2>=footprintZ2)return true;\r\n        if((ox2-ox1)>=placement.l*.7 && (oz2-oz1)>=placement.w*.7)return true;\r\n    }\r\n    return false;\r\n}\r\n\r\nfunction sectionRanges(e){\r\n    const L=e.length;\r\n    const quarter=L\/4;\r\n    return [0,quarter,quarter*2,quarter*3,L];\r\n}\r\n\r\nfunction candidatePositions(piece,orientation,placed,e,mode,gap){\r\n    const [l,w,h]=orientation;\r\n    const ranges=sectionRanges(e);\r\n    const maxH=boundsFor(e).H;\r\n    const positions=[];\r\n    const xs=[];\r\n\r\n    if(mode===\"section\"){\r\n        for(let s=0;s<4;s++){\r\n            const start=ranges[s];\r\n            const end=ranges[s+1];\r\n            if(start+l<=end+gap)xs.push(start);\r\n            xs.push(end-l);\r\n        }\r\n        placed.forEach(function(p){\r\n            const candidates=[p.x+p.l,p.x-gap,p.x+(p.l-l)\/2];\r\n            candidates.forEach(x=>{\r\n                if(x>=-gap&&x+l<=e.length+gap)xs.push(x);\r\n            });\r\n        });\r\n    }else{\r\n        xs.push(0,e.length-l);\r\n        placed.forEach(function(p){\r\n            [p.x+p.l,p.x-gap,p.x+(p.l-l)\/2].forEach(x=>xs.push(x));\r\n        });\r\n    }\r\n\r\n    const ys=[0];\r\n    placed.forEach(function(p){\r\n        ys.push(p.y+p.h);\r\n    });\r\n\r\n    const zs=[0,e.width-w];\r\n    placed.forEach(function(p){\r\n        [p.z+p.w,p.z-gap,p.z+(p.w-w)\/2].forEach(z=>zs.push(z));\r\n    });\r\n\r\n    const uniq=(arr)=>Array.from(new Set(arr.map(v=>Math.round(v))));\r\n\r\n    uniq(xs).forEach(x=>uniq(ys).forEach(y=>uniq(zs).forEach(z=>{\r\n        positions.push({x:x,y:y,z:z,l:l,w:w,h:h});\r\n    })));\r\n\r\n    positions.sort(function(a,b){\r\n        if(a.y!==b.y)return a.y-b.y;\r\n        if(a.z!==b.z)return a.z-b.z;\r\n        return a.x-b.x;\r\n    });\r\n\r\n    return positions;\r\n}\r\n\r\nfunction blocksNonStackableSupport(p,placed,gap){\r\n    if(p.y<=gap)return false;\r\n\r\n    for(const b of placed){\r\n        if(b.stackable!==false)continue;\r\n        const top=b.y+b.h;\r\n        if(Math.abs(top-p.y)>Math.max(gap,2))continue;\r\n\r\n        const xOverlap=Math.min(p.x+p.l,b.x+b.l)-Math.max(p.x,b.x);\r\n        const zOverlap=Math.min(p.z+p.w,b.z+b.w)-Math.max(p.z,b.z);\r\n\r\n        if(xOverlap<=0||zOverlap<=0)continue;\r\n\r\n        if(\r\n            xOverlap>=p.l*.25 ||\r\n            zOverlap>=p.w*.25\r\n        ){\r\n            return true;\r\n        }\r\n    }\r\n\r\n    return false;\r\n}\r\n\r\nfunction placementAllowed(p,placed,e,packMode,gap){\r\n    const maxH=boundsFor(e).H;\r\n    if(p.x < -gap || p.y < -gap || p.z < -gap)return false;\r\n    if(p.x+p.l > e.length+gap || p.y+p.h > maxH+gap || p.z+p.w > e.width+gap)return false;\r\n\r\n    \/* Non-stackable cargo cannot be used as a support surface. *\/\r\n    if(blocksNonStackableSupport(p,placed,gap))return false;\r\n\r\n    if(e.type.startsWith(\"fr\")){\r\n        \/* In-profile flat-rack check: floor width can be used, but overwidth is flagged separately. *\/\r\n    }else{\r\n        const doorCheck=byId(\"nscDoorCheck\").value;\r\n        if(doorCheck===\"strict\" && p.w>e.doorWidth+gap && p.l>e.doorWidth+gap){\r\n            \/* Either width or length must permit a door-entry orientation. *\/\r\n            return false;\r\n        }\r\n        if(doorCheck===\"strict\" && p.h>e.doorHeight+gap && !e.type.startsWith(\"ot\"))return false;\r\n    }\r\n\r\n    for(const b of placed){\r\n        if(overlap(p,b,gap))return false;\r\n    }\r\n\r\n    if(!supportedBy(p,placed,gap))return false;\r\n    return true;\r\n}\r\n\r\nfunction isUniformGridExample(pieces){\r\n    if(!exampleMode || !pieces.length)return false;\r\n\r\n    const first=pieces[0];\r\n\r\n    return pieces.every(function(piece){\r\n        return (\r\n            Math.abs(piece.l-first.l)<0.000001 &&\r\n            Math.abs(piece.w-first.w)<0.000001 &&\r\n            Math.abs(piece.h-first.h)<0.000001 &&\r\n            piece.rotation===\"no-rotate\"\r\n        );\r\n    });\r\n}\r\n\r\nfunction planStructuredSectionExample(pieces,e,gap){\r\n    const placed=[];\r\n    const unplaced=[];\r\n    const max=boundsFor(e);\r\n\r\n    const pools=new Map();\r\n    pieces.forEach(function(piece){\r\n        if(!pools.has(piece.rowId))pools.set(piece.rowId,[]);\r\n        pools.get(piece.rowId).push(piece);\r\n    });\r\n\r\n    \/* Blueprint order is intentionally section-major, then layer-major,\r\n       then back-to-front within the section. This is the requested loading\r\n       sequence and is independent from the normal heuristic sorter. *\/\r\n    exampleBlueprint.forEach(function(cell){\r\n        const pool=pools.get(cell.rowId);\r\n        const piece=pool && pool.length ? pool.shift() : null;\r\n\r\n        if(!piece){\r\n            unplaced.push({\r\n                rowId:cell.rowId,\r\n                shipper:cell.shipper,\r\n                description:\"Example cargo\",\r\n                l:cell.l,\r\n                w:cell.w,\r\n                h:cell.h,\r\n                weight:0,\r\n                maxStack:99,\r\n                rotation:\"no-rotate\",\r\n                stackable:true,\r\n                rollable:false,\r\n                color:SHIPPER_COLORS[0],\r\n                placed:false,\r\n                uid:`example-${cell.order}`\r\n            });\r\n            return;\r\n        }\r\n\r\n        const valid=(\r\n            cell.x>=0 &&\r\n            cell.y>=0 &&\r\n            cell.z>=0 &&\r\n            cell.x+cell.l<=e.length+0.001 &&\r\n            cell.y+cell.h<=max.H+0.001 &&\r\n            cell.z+cell.w<=e.width+0.001\r\n        );\r\n\r\n        if(!valid){\r\n            unplaced.push(piece);\r\n            return;\r\n        }\r\n\r\n        piece.placed=true;\r\n        piece.exampleSection=cell.section+1;\r\n        piece.exampleLayer=cell.layer+1;\r\n        piece.exampleOrder=cell.order+1;\r\n        piece.x=cell.x;\r\n        piece.y=cell.y;\r\n        piece.z=cell.z;\r\n        piece.l=cell.l;\r\n        piece.w=cell.w;\r\n        piece.h=cell.h;\r\n        placed.push(piece);\r\n    });\r\n\r\n    \/* Any unexpected surplus piece becomes unplaced rather than silently\r\n       disappearing from the report. *\/\r\n    pools.forEach(function(pool){\r\n        pool.forEach(function(piece){unplaced.push(piece);});\r\n    });\r\n\r\n    const usedVolume=placed.reduce((s,p)=>s+(p.l*p.w*p.h\/1e9),0);\r\n    const geometricCapacity=(e.length*e.width*max.H)\/1e9;\r\n    const placedWeight=placed.reduce((s,p)=>s+p.weight,0);\r\n    const utilization=geometricCapacity>0\r\n        ? (usedVolume\/geometricCapacity)*100\r\n        : 0;\r\n    const balance=calculateBalance(placed,e);\r\n    const warnings=buildWarnings(e,placed,unplaced,usedVolume,placedWeight,balance);\r\n\r\n    return {\r\n        equipment:e,\r\n        pieces:pieces,\r\n        placed:placed,\r\n        unplaced:unplaced,\r\n        usedVolume:usedVolume,\r\n        placedWeight:placedWeight,\r\n        utilization:Math.min(100,utilization),\r\n        balance:balance,\r\n        warnings:warnings,\r\n        exampleFullCapacity:true,\r\n        exampleSectionSequence:true\r\n    };\r\n}\r\n\r\nfunction planUniformGridExample(pieces,e,gap){\r\n    const placed=[];\r\n    const unplaced=[];\r\n    const max=boundsFor(e);\r\n    const first=pieces[0];\r\n    const tolerance=Math.max(gap,0)+0.01;\r\n\r\n    \/* Use the exact box dimensions supplied by the example generator.\r\n       Because these dimensions were produced from the equipment envelope,\r\n       the grid fills the usable planning envelope without gaps. *\/\r\n    const nx=Math.max(1,Math.round(e.length\/first.l));\r\n    const nz=Math.max(1,Math.round(e.width\/first.w));\r\n    const ny=Math.max(1,Math.round(max.H\/first.h));\r\n\r\n    for(let index=0;index<pieces.length;index++){\r\n        const piece=pieces[index];\r\n        const layer=Math.floor(index\/(nx*nz));\r\n        const withinLayer=index%(nx*nz);\r\n        const zIndex=Math.floor(withinLayer\/nx);\r\n        const xIndex=withinLayer%nx;\r\n\r\n        if(layer>=ny){\r\n            unplaced.push(piece);\r\n            continue;\r\n        }\r\n\r\n        const p={\r\n            x:xIndex*first.l,\r\n            y:layer*first.h,\r\n            z:zIndex*first.w,\r\n            l:first.l,\r\n            w:first.w,\r\n            h:first.h\r\n        };\r\n\r\n        if(\r\n            p.x+p.l > e.length+tolerance ||\r\n            p.y+p.h > max.H+tolerance ||\r\n            p.z+p.w > e.width+tolerance\r\n        ){\r\n            unplaced.push(piece);\r\n            continue;\r\n        }\r\n\r\n        \/* The deterministic grid is equivalent to the normal engine's\r\n           floor-supported, non-overlapping placement for identical boxes. *\/\r\n        piece.placed=true;\r\n        piece.x=p.x;\r\n        piece.y=p.y;\r\n        piece.z=p.z;\r\n        piece.l=p.l;\r\n        piece.w=p.w;\r\n        piece.h=p.h;\r\n        placed.push(piece);\r\n    }\r\n\r\n    const usedVolume=placed.reduce((s,p)=>s+p.l*p.w*p.h\/1e9,0);\r\n    const placedWeight=placed.reduce((s,p)=>s+p.weight,0);\r\n    const geometricCapacity=(e.length*e.width*max.H)\/1e9;\r\n    const utilization=geometricCapacity>0\r\n        ? (usedVolume\/geometricCapacity)*100\r\n        : 0;\r\n    const balance=calculateBalance(placed,e);\r\n    const warnings=buildWarnings(e,placed,unplaced,usedVolume,placedWeight,balance);\r\n\r\n    return {\r\n        equipment:e,\r\n        pieces:pieces,\r\n        placed:placed,\r\n        unplaced:unplaced,\r\n        usedVolume:usedVolume,\r\n        placedWeight:placedWeight,\r\n        utilization:Math.min(100,utilization),\r\n        balance:balance,\r\n        warnings:warnings,\r\n        exampleFullCapacity:true\r\n    };\r\n}\r\n\r\nfunction planLoad(){\r\n    const e=currentEquipment();\r\n    const mode=byId(\"nscPackMode\").value;\r\n    const gap=Math.max(0,safeNum(byId(\"nscGapTolerance\").value));\r\n    const pieces=expandCargo();\r\n\r\n    if(isStructuredSectionExample()){\r\n        return planStructuredSectionExample(pieces,e,gap);\r\n    }\r\n\r\n    if(isUniformGridExample(pieces)){\r\n        return planUniformGridExample(pieces,e,gap);\r\n    }\r\n\r\n    const placed=[];\r\n    const unplaced=[];\r\n    const totalRequestedWeight=getTotalRequestedWeight();\r\n\r\n    if(totalRequestedWeight>e.payload){\r\n        \/* Still attempt packing so the visualization shows physical fit; payload is flagged below. *\/\r\n    }\r\n\r\n    for(const piece of pieces){\r\n        const rotations=rotationsFor(piece);\r\n        let best=null;\r\n        let bestScore=Infinity;\r\n\r\n        for(const rot of rotations){\r\n            const positions=candidatePositions(piece,rot,placed,e,mode,gap);\r\n            for(const pos of positions){\r\n                if(!placementAllowed(pos,placed,e,mode,gap))continue;\r\n\r\n                \/* Prefer lower layers, tighter longitudinal packing and near-center width. *\/\r\n                const centerOffset=Math.abs((pos.z+pos.w\/2)-e.width\/2);\r\n                const score=(pos.y*10)+(pos.x*.8)+(centerOffset*.18)+(pos.z*.1);\r\n                if(score<bestScore){\r\n                    bestScore=score;\r\n                    best=pos;\r\n                }\r\n            }\r\n        }\r\n\r\n        if(best){\r\n            piece.placed=true;\r\n            piece.x=best.x;\r\n            piece.y=best.y;\r\n            piece.z=best.z;\r\n            piece.l=best.l;\r\n            piece.w=best.w;\r\n            piece.h=best.h;\r\n            placed.push(piece);\r\n        }else{\r\n            unplaced.push(piece);\r\n        }\r\n    }\r\n\r\n    const usedVolume=placed.reduce((s,p)=>s+p.l*p.w*p.h\/1e9,0);\r\n    const placedWeight=placed.reduce((s,p)=>s+p.weight,0);\r\n    const utilization=e.capacity?usedVolume\/e.capacity*100:0;\r\n    const balance=calculateBalance(placed,e);\r\n    const warnings=buildWarnings(e,placed,unplaced,usedVolume,placedWeight,balance);\r\n\r\n    return {\r\n        equipment:e,\r\n        pieces:pieces,\r\n        placed:placed,\r\n        unplaced:unplaced,\r\n        usedVolume:usedVolume,\r\n        placedWeight:placedWeight,\r\n        utilization:utilization,\r\n        balance:balance,\r\n        warnings:warnings\r\n    };\r\n}\r\n\r\nfunction calculateBalance(placed,e){\r\n    if(!placed.length)return {status:\"\u2014\",ratio:0,center:0};\r\n    const total=placed.reduce((s,p)=>s+p.weight,0);\r\n    if(!total)return {status:\"NO WEIGHT\",ratio:0,center:e.length\/2};\r\n    const moment=placed.reduce((s,p)=>s+p.weight*(p.x+p.l\/2),0);\r\n    const center=moment\/total;\r\n    const ideal=e.length\/2;\r\n    const delta=center-ideal;\r\n    const ratio=delta\/(e.length\/2);\r\n    const abs=Math.abs(ratio);\r\n    let status=\"BALANCED\";\r\n    if(abs>.20)status=\"REVIEW \u2014 HIGH OFFSET\";\r\n    else if(abs>.10)status=\"REVIEW \u2014 MODERATE OFFSET\";\r\n    return {status:status,ratio:ratio,center:center,delta:delta};\r\n}\r\n\r\nfunction buildWarnings(e,placed,unplaced,usedVolume,placedWeight,balance){\r\n    const warnings=[];\r\n    if(unplaced.length)warnings.push(`${unplaced.length} cargo piece(s) could not be placed with the current dimensions, rotation rules and packing constraints.`);\r\n    if(placedWeight>e.payload)warnings.push(`Planned placed cargo weight ${fmt(placedWeight,2)} kg exceeds the equipment payload reference of ${fmt(e.payload,0)} kg.`);\r\n    if(e.capacity&&usedVolume>e.capacity && !exampleMode)warnings.push(`Calculated placed geometric volume exceeds the nominal container volume.`);\r\n    if(balance&&Math.abs(balance.ratio)>.10)warnings.push(`Estimated longitudinal center of gravity is offset from the container midpoint (${fmt(balance.delta\/1000,2)} m). Review weight distribution and securing.`);\r\n\r\n    placed.forEach(function(p){\r\n        if(p.rollable===true){\r\n            warnings.push(`${p.description} (${p.shipper}) is marked rollable. Review chocking, blocking, lashing or other securing requirements before actual stuffing.`);\r\n        }\r\n        if(p.stackable===false){\r\n            warnings.push(`${p.description} (${p.shipper}) is marked non-stackable. The planner will not use it as a support surface for another cargo piece.`);\r\n        }\r\n        if(e.type.startsWith(\"fr\")){\r\n            if(p.w>e.minWidth)warnings.push(`${p.description} exceeds the Flat Rack minimum side-width profile (${fmt(e.minWidth,0)} mm) and may be overwidth \/ OOG depending on orientation.`);\r\n            if(p.h>e.height)warnings.push(`${p.description} exceeds the in-profile Flat Rack height reference (${fmt(e.height,0)} mm) and should be treated as potential overheight \/ OOG.`);\r\n        }\r\n        if(e.type.startsWith(\"ot\") && p.h>e.height && p.h<=(e.heightNoBars||e.height)){\r\n            \/* allowed only with open-top height; informational *\/\r\n        }\r\n    });\r\n\r\n    return warnings;\r\n}\r\n\r\n\/* =========================================================\r\n   3D RENDER LOAD\r\n========================================================= *\/\r\nfunction renderPlan(plan){\r\n    currentPlan=plan||null;\r\n\r\n    if(renderMode===\"canvas3d\"){\r\n        renderFallbackScene(plan);\r\n        return;\r\n    }\r\n\r\n    if(renderMode===\"none\")return;\r\n    if(!cargoGroup||!sectionGroup)return;\r\n    clearGroup(cargoGroup);\r\n    clearGroup(sectionGroup);\r\n    renderContainer();\r\n\r\n    const e=plan.equipment;\r\n    const scale=.001;\r\n\r\n    \/* Section guides *\/\r\n    if(byId(\"nscPackMode\").value===\"section\" && !e.type.startsWith(\"fr\")){\r\n        const ranges=sectionRanges(e);\r\n        for(let i=1;i<4;i++){\r\n            const x=(ranges[i]-e.length\/2)*scale;\r\n            const geo=new THREE.BufferGeometry();\r\n            geo.setAttribute(\"position\",new THREE.Float32BufferAttribute([\r\n                x,0.012,-e.width\/2*scale,\r\n                x,0.012,e.width\/2*scale\r\n            ],3));\r\n            const mat=new THREE.LineBasicMaterial({color:0xc9a227,transparent:true,opacity:.45});\r\n            sectionGroup.add(new THREE.LineSegments(geo,mat));\r\n        }\r\n    }\r\n\r\n    plan.placed.forEach(function(p){\r\n        const mat=new THREE.MeshStandardMaterial({\r\n            color:new THREE.Color(p.color),\r\n            transparent:true,\r\n            opacity:.82,\r\n            roughness:.72,\r\n            metalness:.05\r\n        });\r\n        const mesh=new THREE.Mesh(\r\n            new THREE.BoxGeometry(p.l*scale,p.h*scale,p.w*scale),\r\n            mat\r\n        );\r\n        mesh.renderOrder=10;\r\n        mesh.position.set(\r\n            (p.x+p.l\/2-e.length\/2)*scale,\r\n            (p.y+p.h\/2)*scale,\r\n            (p.z+p.w\/2-e.width\/2)*scale\r\n        );\r\n        mesh.userData={shipper:p.shipper,description:p.description,weight:p.weight,uid:p.uid,stackable:p.stackable!==false,rollable:p.rollable===true};\r\n        cargoGroup.add(mesh);\r\n\r\n        const edges=new THREE.LineSegments(\r\n            new THREE.EdgesGeometry(mesh.geometry),\r\n            new THREE.LineBasicMaterial({color:0x172b3a,transparent:true,opacity:.55,depthTest:true})\r\n        );\r\n        edges.renderOrder=11;\r\n        mesh.add(edges);\r\n    });\r\n\r\n    const maxDim=Math.max(e.length,e.width,e.height)*scale;\r\n    if(camera&&controls){\r\n        camera.position.set(-maxDim*1.05,maxDim*.8,maxDim*1.1);\r\n        controls.target.set(0,(Math.min(boundsFor(e).H,1800)*scale)\/2,0);\r\n        camera.zoom=1;\r\n        controls.update();\r\n        if(!controls.autoRotate)setViewControlState(false);\r\n    }\r\n    resize3D();\r\n    if(renderer&&scene&&camera){\r\n        if(controls)controls.update();\r\n        renderer.render(scene,camera);\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   REPORT\r\n========================================================= *\/\r\nfunction resetReport(){\r\n    byId(\"nsc3dBadge\").textContent=\"NOT RUN\";\r\n    byId(\"nsc3dBadge\").className=\"nsc-lp-fit-badge\";\r\n    byId(\"nscPlacedPieces\").textContent=`0 \/ ${fmt(getTotalRequestedPieces(),0)} pcs`;\r\n    byId(\"nscUnplacedPieces\").textContent=\"0 pcs\";\r\n    byId(\"nscPayloadValue\").textContent=`0 \/ ${fmt(currentEquipment().payload,0)} kg`;\r\n    byId(\"nscVolumeValue\").textContent=\"0%\";\r\n    byId(\"nscBalanceValue\").textContent=\"\u2014\";\r\n    byId(\"nscShipperList\").innerHTML=\"\";\r\n    if(byId(\"nscHandlingSummary\"))byId(\"nscHandlingSummary\").textContent=\"Stackable \/ rollable status will appear after you create the plan.\";\r\n    byId(\"nscWarnings\").textContent=\"No plan has been generated.\";\r\n    byId(\"nscMainAlert\").className=\"nsc-lp-alert\";\r\n    byId(\"nscMainAlert\").textContent=\"Add cargo lots and run the planner to generate a 3D loading proposal.\";\r\n}\r\n\r\nfunction renderReport(plan){\r\n    updateTopKPIs({\r\n        placed:plan.placed.length,\r\n        unplaced:plan.unplaced.length,\r\n        placedWeight:plan.placedWeight,\r\n        usedVolumePct:plan.utilization,\r\n        balance:plan.balance.status\r\n    });\r\n\r\n    const fit=plan.unplaced.length===0 && plan.placedWeight<=plan.equipment.payload;\r\n    const badge=byId(\"nsc3dBadge\");\r\n    badge.textContent=fit?\"ALL CARGO FITS\":\"REVIEW PLAN\";\r\n    badge.className=\"nsc-lp-fit-badge\"+(fit?\"\":\" warning\");\r\n    if(plan.placed.length===0){badge.textContent=\"NO PLACEMENT\";badge.className=\"nsc-lp-fit-badge error\";}\r\n\r\n    const shipperMap={};\r\n    plan.placed.forEach(p=>{\r\n        if(!shipperMap[p.shipper])shipperMap[p.shipper]={count:0,weight:0,color:p.color};\r\n        shipperMap[p.shipper].count++;\r\n        shipperMap[p.shipper].weight+=p.weight;\r\n    });\r\n    plan.unplaced.forEach(p=>{\r\n        if(!shipperMap[p.shipper])shipperMap[p.shipper]={count:0,weight:0,color:p.color};\r\n        shipperMap[p.shipper].unplaced=(shipperMap[p.shipper].unplaced||0)+1;\r\n    });\r\n\r\n    const list=byId(\"nscShipperList\");\r\n    list.innerHTML=\"\";\r\n    Object.keys(shipperMap).forEach(function(name){\r\n        const s=shipperMap[name];\r\n        const div=document.createElement(\"div\");\r\n        div.className=\"nsc-lp-shipper-row\";\r\n        div.innerHTML=`\r\n            <div class=\"nsc-lp-shipper-name\">\r\n                <span class=\"nsc-lp-shipper-dot\" style=\"background:${escapeHtml(s.color)}\"><\/span>\r\n                <span>${escapeHtml(name)}<\/span>\r\n            <\/div>\r\n            <strong>${fmt(s.count,0)} pcs \u00b7 ${fmt(s.weight,2)} kg${s.unplaced?` \u00b7 ${fmt(s.unplaced,0)} unplaced`:\"\"}<\/strong>\r\n        `;\r\n        list.appendChild(div);\r\n    });\r\n\r\n    if(!Object.keys(shipperMap).length){\r\n        list.innerHTML=`<div style=\"color:#8795a3;font-size:9px;\">No placed shipper cargo yet.<\/div>`;\r\n    }\r\n\r\n    const handlingSummary=byId(\"nscHandlingSummary\");\r\n    if(handlingSummary){\r\n        const stackableCount=plan.placed.filter(p=>p.stackable!==false).length;\r\n        const nonStackableCount=plan.placed.filter(p=>p.stackable===false).length;\r\n        const rollableCount=plan.placed.filter(p=>p.rollable===true).length;\r\n        handlingSummary.innerHTML=`<strong>${fmt(stackableCount,0)}<\/strong> stackable \u00b7 <strong>${fmt(nonStackableCount,0)}<\/strong> non-stackable \u00b7 <strong>${fmt(rollableCount,0)}<\/strong> rollable`;\r\n    }\r\n\r\n    const warn=byId(\"nscWarnings\");\r\n    if(plan.warnings.length){\r\n        warn.className=\"nsc-lp-alert warning\";\r\n        warn.innerHTML=plan.warnings.map(w=>`<div style=\"margin-bottom:5px;\">\u2022 ${escapeHtml(w)}<\/div>`).join(\"\");\r\n    }else{\r\n        warn.className=\"nsc-lp-alert success\";\r\n        warn.textContent=\"No blocking packing warnings were generated by the heuristic engine. Physical stuffing and securing still require operational verification.\";\r\n    }\r\n\r\n    const main=byId(\"nscMainAlert\");\r\n    main.className=fit?\"nsc-lp-alert success\":\"nsc-lp-alert warning\";\r\n    main.textContent=fit\r\n        ? `All requested cargo was placed: ${plan.placed.length} of ${plan.pieces.length} pieces. Weight is within the equipment reference.`\r\n        : `Some cargo still needs review: ${plan.placed.length} of ${plan.pieces.length} pieces were placed.`;\r\n}\r\n\r\n\r\n\/* =========================================================\r\n   V2.4 EQUIPMENT-SPECIFIC RULE ENGINE\r\n   ---------------------------------------------------------\r\n   LOCKED DRY RULES\r\n   - Applies unchanged to 20GP \/ 40GP \/ 40HC.\r\n   - Section-first, then layer-first, grouped shipper loading.\r\n   - Normal user cargo uses the existing packing engine.\r\n\r\n   OPEN TOP RULES\r\n   - Front = door\/opening end.\r\n   - Back = closed inside end.\r\n   - Left\/right side walls remain.\r\n   - Top is open; cargo may exceed wall height when top-loaded.\r\n   - Height above side-wall height = OUT GAUGE \/ OVERHEIGHT.\r\n   - Cargo that stays within length x width x wall height = IN GAUGE.\r\n   - Door height is not used to reject an OOG top-loaded piece.\r\n\r\n   FLAT RACK RULES\r\n   - No door.\r\n   - Front and back are structural end walls with corner posts \/ end frames.\r\n   - Left\/right sides are open.\r\n   - Top is open.\r\n   - Maximum cargo length is the equipment platform length.\r\n   - Width and height may exceed the frame profile and become OUT GAUGE.\r\n   - Cargo within platform width and frame height = IN GAUGE.\r\n========================================================= *\/\r\n\r\n\/* Preserve the existing engine as the dry-container implementation. *\/\r\nconst nscV23 = {\r\n    boundsFor: boundsFor,\r\n    candidatePositions: candidatePositions,\r\n    placementAllowed: placementAllowed,\r\n    isUniformGridExample: isUniformGridExample,\r\n    isStructuredSectionExample: isStructuredSectionExample,\r\n    planStructuredSectionExample: planStructuredSectionExample,\r\n    planUniformGridExample: planUniformGridExample,\r\n    planLoad: planLoad,\r\n    buildWarnings: buildWarnings,\r\n    renderCargoTable: renderCargoTable,\r\n    updateEquipmentUI: updateEquipmentUI,\r\n    loadExample: loadExample,\r\n    renderContainer: renderContainer,\r\n    drawContainerFallback: drawContainerFallback,\r\n    renderPlan: renderPlan,\r\n    renderFallbackScene: renderFallbackScene,\r\n    renderReport: renderReport\r\n};\r\n\r\n\/* =========================================================\r\n   EQUIPMENT FAMILY\r\n========================================================= *\/\r\nfunction equipmentFamily(e){\r\n    if(!e)return \"dry\";\r\n    if(e.type && e.type.startsWith(\"fr\"))return \"flat-rack\";\r\n    if(e.type && e.type.startsWith(\"ot\"))return \"open-top\";\r\n    return \"dry\";\r\n}\r\n\r\nfunction isDryEquipment(e){\r\n    return equipmentFamily(e)===\"dry\";\r\n}\r\n\r\nfunction isOpenTopEquipment(e){\r\n    return equipmentFamily(e)===\"open-top\";\r\n}\r\n\r\nfunction isFlatRackEquipment(e){\r\n    return equipmentFamily(e)===\"flat-rack\";\r\n}\r\n\r\n\/* Add explicit special-equipment planning limits. *\/\r\nObject.keys(EQUIPMENT).forEach(function(key){\r\n    const e=EQUIPMENT[key];\r\n\r\n    if(isOpenTopEquipment(e)){\r\n        e.wallHeight=e.height;\r\n        e.topLoad=true;\r\n        e.maxOogHeight=e.maxOogHeight||6000;\r\n        e.ruleLabel=\"Open Top\";\r\n    }\r\n\r\n    if(isFlatRackEquipment(e)){\r\n        e.maxCargoLength=e.length;\r\n        e.maxOogWidth=e.maxOogWidth||6000;\r\n        e.maxOogHeight=e.maxOogHeight||6000;\r\n        e.ruleLabel=\"Flat Rack\";\r\n    }\r\n\r\n    if(isDryEquipment(e)){\r\n        e.ruleLabel=\"Standard Dry\";\r\n    }\r\n});\r\n\r\n\/* =========================================================\r\n   GAUGE CLASSIFICATION\r\n========================================================= *\/\r\nfunction gaugeForPiece(piece,e,tolerance=0){\r\n    if(!piece||!e)return {status:\"UNKNOWN\",reason:\"Equipment not available.\"};\r\n\r\n    tolerance=Math.max(0,safeNum(tolerance));\r\n\r\n    if(isDryEquipment(e)){\r\n        const overLength=piece.l>e.length+tolerance;\r\n        const overWidth=piece.w>e.width+tolerance;\r\n        const overHeight=piece.h>e.height+tolerance;\r\n\r\n        if(overLength||overWidth||overHeight){\r\n            return {\r\n                status:\"OUT GAUGE\",\r\n                reason: overLength\r\n                    ? \"Length exceeds the internal container length.\"\r\n                    : overWidth\r\n                        ? \"Width exceeds the internal container width.\"\r\n                        : \"Height exceeds the internal container height.\"\r\n            };\r\n        }\r\n\r\n        return {\r\n            status:\"IN GAUGE\",\r\n            reason:\"Cargo dimensions are within the standard internal envelope.\"\r\n        };\r\n    }\r\n\r\n    if(isOpenTopEquipment(e)){\r\n        const overLength=piece.l>e.length+tolerance;\r\n        const overWidth=piece.w>e.width+tolerance;\r\n\r\n        if(overLength){\r\n            return {\r\n                status:\"BLOCKED\",\r\n                reason:\"Length exceeds the Open Top internal length.\"\r\n            };\r\n        }\r\n\r\n        if(overWidth){\r\n            return {\r\n                status:\"BLOCKED\",\r\n                reason:\"Width exceeds the Open Top side-wall width.\"\r\n            };\r\n        }\r\n\r\n        if(piece.h>e.wallHeight+tolerance){\r\n            if(!roofLoadPossible(piece,e,tolerance)){\r\n                return {\r\n                    status:\"BLOCKED\",\r\n                    reason:\"Overheight cargo does not fit through the available Open Top roof opening in this orientation.\"\r\n                };\r\n            }\r\n\r\n            return {\r\n                status:\"OUT GAUGE\",\r\n                reason:\"Overheight above the Open Top side-wall height; top-loading profile.\"\r\n            };\r\n        }\r\n\r\n        return {\r\n            status:\"IN GAUGE\",\r\n            reason:\"Cargo remains within the Open Top side-wall height.\"\r\n        };\r\n    }\r\n\r\n    if(isFlatRackEquipment(e)){\r\n        if(piece.l>e.maxCargoLength+tolerance){\r\n            return {\r\n                status:\"BLOCKED\",\r\n                reason:\"Length exceeds the Flat Rack platform length between end frames.\"\r\n            };\r\n        }\r\n\r\n        const gaugeWidth=e.minWidth||e.width;\r\n        const outWidth=piece.w>gaugeWidth+tolerance;\r\n        const outHeight=piece.h>e.height+tolerance;\r\n\r\n        if(outWidth||outHeight){\r\n            let reason=\"Flat Rack OOG\";\r\n            if(outWidth&&outHeight)reason=\"Overwidth and overheight beyond the Flat Rack profile.\";\r\n            else if(outWidth)reason=\"Overwidth beyond the Flat Rack frame profile.\";\r\n            else if(outHeight)reason=\"Overheight beyond the Flat Rack frame profile.\";\r\n            return {\r\n                status:\"OUT GAUGE\",\r\n                reason:reason\r\n            };\r\n        }\r\n\r\n        return {\r\n            status:\"IN GAUGE\",\r\n            reason:\"Cargo remains within the Flat Rack platform\/frame profile.\"\r\n        };\r\n    }\r\n\r\n    return {status:\"UNKNOWN\",reason:\"Unable to classify gauge.\"};\r\n}\r\n\r\nfunction gaugeBadgeHtml(piece,e){\r\n    const g=gaugeForPiece(piece,e,Math.max(0,safeNum(byId(\"nscGapTolerance\").value)));\r\n    const klass=g.status===\"IN GAUGE\"\r\n        ? \"nsc-lp-gauge-badge nsc-lp-gauge-in\"\r\n        : g.status===\"OUT GAUGE\"\r\n            ? \"nsc-lp-gauge-badge nsc-lp-gauge-out\"\r\n            : \"nsc-lp-gauge-badge nsc-lp-gauge-blocked\";\r\n    return `<span class=\"${klass}\" title=\"${escapeHtml(g.reason)}\">${escapeHtml(g.status)}<\/span>`;\r\n}\r\n\r\n\/* =========================================================\r\n   EQUIPMENT RULE UI\r\n========================================================= *\/\r\nfunction updateEquipmentRuleUI(){\r\n    const e=currentEquipment();\r\n    const family=equipmentFamily(e);\r\n    const box=byId(\"nscEquipmentRuleBox\");\r\n    const button=byId(\"nscLoadExample\");\r\n\r\n    if(box){\r\n        if(family===\"dry\"){\r\n            box.innerHTML=\r\n                \"<strong>Standard dry-container rule:<\/strong> \" +\r\n                \"20GP \/ 40GP \/ 40HC use the locked section-first and layer-first workflow. \" +\r\n                \"Cargo is grouped by shipper and each shipper is completed before the next shipper begins.\";\r\n        }else if(family===\"open-top\"){\r\n            box.innerHTML=\r\n                \"<strong>Open Top rule:<\/strong> \" +\r\n                \"The back and front\/door are framed, left\/right side walls define the normal width, \" +\r\n                \"and the roof is open. Overheight cargo may be top-loaded when its footprint can pass through the available roof opening. Height above the side-wall height is <strong>OUT GAUGE \/ OVERHEIGHT<\/strong>.\";\r\n        }else{\r\n            box.innerHTML=\r\n                \"<strong>Flat Rack rule:<\/strong> \" +\r\n                \"There is no door. The front and back are structural end walls with corner posts \/ end frames, while the left, right and top are open. \" +\r\n                \"Cargo length is hard-limited by the platform length; width and height may extend beyond the end-frame profile and become <strong>OUT GAUGE<\/strong>.\";\r\n        }\r\n    }\r\n\r\n    if(button){\r\n        if(family===\"dry\"){\r\n            button.textContent=\"Load Multi-Shipper Full-Capacity Example\";\r\n        }else if(family===\"open-top\"){\r\n            button.textContent=\"Load Open Top OOG Example\";\r\n        }else{\r\n            button.textContent=\"Load Flat Rack OOG Example\";\r\n        }\r\n    }\r\n\r\n    const legend= root.querySelector(\".nsc-lp-visual-legend\");\r\n    if(legend){\r\n        if(family===\"dry\"){\r\n            legend.innerHTML=\r\n                '<span><i class=\"nsc-lp-legend-dot nsc-lp-legend-ground\"><\/i>Loading yard<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot nsc-lp-legend-frame\"><\/i>Container<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot\" style=\"background:#f59e0b;\"><\/i>Door<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot nsc-lp-legend-cargo\"><\/i>Cargo (colors = shippers)<\/span>';\r\n        }else if(family===\"open-top\"){\r\n            legend.innerHTML=\r\n                '<span><i class=\"nsc-lp-legend-dot nsc-lp-legend-ground\"><\/i>Loading yard<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot nsc-lp-legend-frame\"><\/i>Side \/ end walls<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot\" style=\"background:#f59e0b;\"><\/i>Door \/ top-loading zone<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot\" style=\"background:#c83d3d;\"><\/i>OUT GAUGE \/ overheight<\/span>';\r\n        }else{\r\n            legend.innerHTML=\r\n                '<span><i class=\"nsc-lp-legend-dot nsc-lp-legend-ground\"><\/i>Loading yard<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot nsc-lp-legend-frame\"><\/i>End walls \/ corner posts<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot\" style=\"background:#f59e0b;\"><\/i>Front end<\/span>' +\r\n                '<span><i class=\"nsc-lp-legend-dot\" style=\"background:#c83d3d;\"><\/i>OUT GAUGE \/ OOG<\/span>';\r\n        }\r\n    }\r\n}\r\n\r\n\/* Keep existing database display, but add special rule text. *\/\r\nfunction specialUpdateEquipmentUI(){\r\n    nscV23.updateEquipmentUI();\r\n    updateEquipmentRuleUI();\r\n}\r\n\r\n\/* =========================================================\r\n   CARGO TABLE OVERRIDE\r\n========================================================= *\/\r\nfunction specialRenderCargoTable(){\r\n    const body=byId(\"nscCargoBody\");\r\n    if(!body)return;\r\n\r\n    body.innerHTML=\"\";\r\n\r\n    const e=currentEquipment();\r\n\r\n    cargoRows.forEach(function(row){\r\n        const tr=document.createElement(\"tr\");\r\n        tr.dataset.id=String(row.id);\r\n        tr.innerHTML=`\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"shipper\" value=\"${escapeHtml(row.shipper)}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"description\" value=\"${escapeHtml(row.description)}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"length\" type=\"number\" min=\"0.1\" step=\"0.1\" value=\"${row.length\/10}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"width\" type=\"number\" min=\"0.1\" step=\"0.1\" value=\"${row.width\/10}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"height\" type=\"number\" min=\"0.1\" step=\"0.1\" value=\"${row.height\/10}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"qty\" type=\"number\" min=\"1\" step=\"1\" value=\"${row.qty}\"><\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"weight\" type=\"number\" min=\"0\" step=\"0.01\" value=\"${row.weight}\"><\/td>\r\n            <td>\r\n                <select class=\"nsc-lp-mini-select\" data-field=\"rotation\">\r\n                    <option value=\"all\" ${row.rotation===\"all\"?\"selected\":\"\"}>Any<\/option>\r\n                    <option value=\"no-rotate\" ${row.rotation===\"no-rotate\"?\"selected\":\"\"}>No Rotate<\/option>\r\n                    <option value=\"floor\" ${row.rotation===\"floor\"?\"selected\":\"\"}>Floor Only<\/option>\r\n                <\/select>\r\n            <\/td>\r\n            <td>\r\n                <select class=\"nsc-lp-mini-select\" data-field=\"stackable\">\r\n                    <option value=\"yes\" ${row.stackable!==false?\"selected\":\"\"}>Yes<\/option>\r\n                    <option value=\"no\" ${row.stackable===false?\"selected\":\"\"}>No<\/option>\r\n                <\/select>\r\n            <\/td>\r\n            <td>\r\n                <select class=\"nsc-lp-mini-select\" data-field=\"rollable\">\r\n                    <option value=\"no\" ${row.rollable!==true?\"selected\":\"\"}>No<\/option>\r\n                    <option value=\"yes\" ${row.rollable===true?\"selected\":\"\"}>Yes<\/option>\r\n                <\/select>\r\n            <\/td>\r\n            <td><input class=\"nsc-lp-mini-input\" data-field=\"maxStack\" type=\"number\" min=\"1\" step=\"1\" value=\"${row.maxStack}\" ${row.stackable===false?\"disabled\":\"\"}><\/td>\r\n            <td class=\"nsc-gauge-cell\">${gaugeBadgeHtml(row,e)}<\/td>\r\n            <td><input class=\"nsc-lp-color\" data-field=\"color\" type=\"color\" value=\"${row.color}\"><\/td>\r\n            <td><button class=\"nsc-lp-row-remove\" type=\"button\" title=\"Remove\">\u00d7<\/button><\/td>\r\n        `;\r\n        body.appendChild(tr);\r\n    });\r\n\r\n    body.querySelectorAll(\"tr\").forEach(function(tr){\r\n        const row=cargoRows.find(r=>String(r.id)===tr.dataset.id);\r\n        if(!row)return;\r\n\r\n        tr.querySelectorAll(\"[data-field]\").forEach(function(input){\r\n            input.addEventListener(\"input\",function(){\r\n                exampleMode=false;\r\n                exampleBlueprint=null;\r\n\r\n                const field=input.dataset.field;\r\n\r\n                if(field===\"shipper\"||field===\"description\"||field===\"rotation\"||field===\"color\"){\r\n                    row[field]=input.value;\r\n                }else if(field===\"stackable\"){\r\n                    row.stackable=input.value!==\"no\";\r\n                    if(!row.stackable)row.maxStack=1;\r\n                    else if(row.maxStack<1)row.maxStack=99;\r\n                    specialRenderCargoTable();\r\n                    return;\r\n                }else if(field===\"rollable\"){\r\n                    row.rollable=input.value===\"yes\";\r\n                }else if(field===\"qty\"||field===\"maxStack\"){\r\n                    row[field]=Math.max(1,Math.floor(safeNum(input.value)||1));\r\n                }else{\r\n                    if(field===\"length\"||field===\"width\"||field===\"height\"){\r\n                        row[field]=safeNum(input.value)*10;\r\n                    }else{\r\n                        row[field]=safeNum(input.value);\r\n                    }\r\n                }\r\n\r\n                const gaugeCell=tr.querySelector(\".nsc-gauge-cell\");\r\n                if(gaugeCell)gaugeCell.innerHTML=gaugeBadgeHtml(row,currentEquipment());\r\n\r\n                updateTopKPIs();\r\n            });\r\n        });\r\n\r\n        const remove=tr.querySelector(\".nsc-lp-row-remove\");\r\n        if(remove){\r\n            remove.addEventListener(\"click\",function(){\r\n                removeCargoRow(row.id);\r\n            });\r\n        }\r\n    });\r\n\r\n    updateTopKPIs();\r\n}\r\n\r\n\/* =========================================================\r\n   EQUIPMENT-SPECIFIC BOUNDS\r\n========================================================= *\/\r\nfunction specialBoundsFor(e){\r\n    if(isDryEquipment(e)){\r\n        return {\r\n            L:e.length,\r\n            W:e.width,\r\n            H:e.height,\r\n            strictHeight:e.height,\r\n            doorWidth:e.doorWidth||null,\r\n            doorHeight:e.doorHeight||null\r\n        };\r\n    }\r\n\r\n    if(isOpenTopEquipment(e)){\r\n        return {\r\n            L:e.length,\r\n            W:e.width,\r\n            H:e.wallHeight,\r\n            maxOogHeight:e.maxOogHeight,\r\n            strictHeight:e.wallHeight,\r\n            doorWidth:e.doorWidth||null,\r\n            doorHeight:e.doorHeight||null,\r\n            roofOpen:true\r\n        };\r\n    }\r\n\r\n    if(isFlatRackEquipment(e)){\r\n        return {\r\n            L:e.length,\r\n            W:e.width,\r\n            H:e.height,\r\n            maxOogHeight:e.maxOogHeight,\r\n            strictHeight:e.height,\r\n            strictWidth:e.minWidth||e.width,\r\n            maxCargoLength:e.maxCargoLength,\r\n            openSides:true\r\n        };\r\n    }\r\n\r\n    return nscV23.boundsFor(e);\r\n}\r\n\r\n\/* =========================================================\r\n   SPECIAL CANDIDATE POSITIONS\r\n========================================================= *\/\r\nfunction specialCandidatePositions(piece,orientation,placed,e,mode,gap){\r\n    if(isDryEquipment(e)){\r\n        return nscV23.candidatePositions(piece,orientation,placed,e,mode,gap);\r\n    }\r\n\r\n    const l=orientation[0], w=orientation[1], h=orientation[2];\r\n    const positions=[];\r\n\r\n    \/*\r\n       SPECIAL CENTER LOCK\r\n       ---------------------------------------------\r\n       Open Top and Flat Rack must prefer the\r\n       longitudinal center after any cargo edit\/re-plan.\r\n       Dry \/ GP equipment never reaches this branch.\r\n    *\/\r\n    const centerX=(e.length-l)\/2;\r\n    const xs=[centerX,0,e.length-l];\r\n    placed.forEach(function(p){\r\n        xs.push(p.x+p.l);\r\n        xs.push(p.x-gap);\r\n        xs.push(p.x+(p.l-l)\/2);\r\n    });\r\n\r\n    const ys=[0];\r\n    placed.forEach(function(p){\r\n        ys.push(p.y+p.h);\r\n    });\r\n\r\n    let zs=[];\r\n\r\n    if(isOpenTopEquipment(e)){\r\n        zs=[0,e.width-w,(e.width-w)\/2];\r\n    }else{\r\n        \/* Flat Rack: center cargo on the platform even when it is wider\r\n           than the platform, allowing negative z for symmetric OOG. *\/\r\n        zs=[\r\n            (e.width-w)\/2,\r\n            0,\r\n            e.width-w,\r\n            -e.width*0.5,\r\n            e.width*0.5\r\n        ];\r\n        placed.forEach(function(p){\r\n            zs.push(p.z+p.w);\r\n            zs.push(p.z-gap);\r\n            zs.push(p.z+(p.w-w)\/2);\r\n        });\r\n    }\r\n\r\n    const uniq=function(arr){\r\n        return Array.from(new Set(\r\n            arr\r\n            .filter(Number.isFinite)\r\n            .map(function(v){return Math.round(v);})\r\n        ));\r\n    };\r\n\r\n    uniq(xs).forEach(function(x){\r\n        uniq(ys).forEach(function(y){\r\n            uniq(zs).forEach(function(z){\r\n                positions.push({\r\n                    x:x,\r\n                    y:y,\r\n                    z:z,\r\n                    l:l,\r\n                    w:w,\r\n                    h:h\r\n                });\r\n            });\r\n        });\r\n    });\r\n\r\n    positions.sort(function(a,b){\r\n        if(a.y!==b.y)return a.y-b.y;\r\n\r\n        \/*\r\n           Center both special-equipment axes before using the\r\n           fallback edge positions. This is intentionally scoped\r\n           to Open Top \/ Flat Rack only.\r\n        *\/\r\n        const aCenterX=Math.abs((a.x+a.l\/2)-e.length\/2);\r\n        const bCenterX=Math.abs((b.x+b.l\/2)-e.length\/2);\r\n        if(aCenterX!==bCenterX)return aCenterX-bCenterX;\r\n\r\n        const aCenterZ=Math.abs((a.z+a.w\/2)-e.width\/2);\r\n        const bCenterZ=Math.abs((b.z+b.w\/2)-e.width\/2);\r\n        if(aCenterZ!==bCenterZ)return aCenterZ-bCenterZ;\r\n\r\n        return a.x-b.x;\r\n    });\r\n\r\n    return positions;\r\n}\r\n\r\n\r\nfunction roofLoadPossible(piece,e,tolerance=0){\r\n    if(!isOpenTopEquipment(e))return true;\r\n\r\n    const roofWidth=e.roofWidth||e.width;\r\n    const roofLength=e.roofLength||e.length;\r\n    const t=Math.max(0,safeNum(tolerance));\r\n\r\n    const direct=piece.l<=roofLength+t && piece.w<=roofWidth+t;\r\n    const rotated=piece.w<=roofLength+t && piece.l<=roofWidth+t;\r\n\r\n    return direct||rotated;\r\n}\r\n\r\n\/* =========================================================\r\n   SPECIAL PLACEMENT RULES\r\n========================================================= *\/\r\nfunction specialPlacementAllowed(p,placed,e,packMode,gap){\r\n    if(isDryEquipment(e)){\r\n        return nscV23.placementAllowed(p,placed,e,packMode,gap);\r\n    }\r\n\r\n    gap=Math.max(0,safeNum(gap));\r\n\r\n    if(p.y<-gap)return false;\r\n\r\n    if(isOpenTopEquipment(e)){\r\n        \/* Floor footprint remains inside the side walls. Height may exceed\r\n           wall height because the roof is open. *\/\r\n        if(p.x<-gap || p.x+p.l>e.length+gap)return false;\r\n        if(p.z<-gap || p.z+p.w>e.width+gap)return false;\r\n        if(p.y+p.h>e.maxOogHeight+gap)return false;\r\n\r\n        if(blocksNonStackableSupport(p,placed,gap))return false;\r\n\r\n        \/* Overheight cargo must still pass through the available roof opening. *\/\r\n        if(p.h>e.wallHeight+gap && !roofLoadPossible(p,e,gap))return false;\r\n\r\n        \/* Open Top top-loading OOG can bypass door-height restriction. *\/\r\n        if(p.h<=e.wallHeight+gap){\r\n            if(byId(\"nscDoorCheck\").value===\"strict\" && e.doorWidth && p.w>e.doorWidth+gap && p.l>e.doorWidth+gap){\r\n                return false;\r\n            }\r\n            if(byId(\"nscDoorCheck\").value===\"strict\" && e.doorHeight && p.h>e.doorHeight+gap){\r\n                return false;\r\n            }\r\n        }\r\n\r\n    }else if(isFlatRackEquipment(e)){\r\n        \/* Length remains hard-limited by the platform\/end frames. Width\r\n           and height may exceed the profile for OOG cargo. *\/\r\n        if(p.x<-gap || p.x+p.l>e.maxCargoLength+gap)return false;\r\n\r\n        const halfAllowance=e.maxOogWidth*0.5;\r\n        if(p.z<-halfAllowance-gap)return false;\r\n        if(p.z+p.w>e.width+halfAllowance+gap)return false;\r\n\r\n        if(p.y+p.h>e.maxOogHeight+gap)return false;\r\n\r\n        if(blocksNonStackableSupport(p,placed,gap))return false;\r\n\r\n    }\r\n\r\n    for(const b of placed){\r\n        if(overlap(p,b,gap))return false;\r\n    }\r\n\r\n    if(!supportedBy(p,placed,gap))return false;\r\n\r\n    return true;\r\n}\r\n\r\n\/* =========================================================\r\n   SPECIAL EXAMPLES\r\n========================================================= *\/\r\nfunction loadOpenTopExample(){\r\n    clearCargoRows();\r\n    exampleMode=true;\r\n    exampleBlueprint=[];\r\n\r\n    const e=currentEquipment();\r\n\r\n    const inGaugeHeight=Math.max(1800,Math.min(e.wallHeight-120,e.wallHeight-40));\r\n\r\n    \/*\r\n       OPEN TOP EXAMPLE WIDTH LOCK\r\n       ----------------------------\r\n       The example must never create a width that cannot pass through the\r\n       physical Open Top roof opening. For overheight cargo, roofWidth is the\r\n       relevant transverse loading limit. Use the tighter of the container\r\n       internal width and the roof opening width, with a small visual\/planning\r\n       allowance so every example remains placeable in W without changing H.\r\n    *\/\r\n    const openTopExampleWidthLimit=Math.max(300,\r\n        Math.min(e.width,e.roofWidth||e.width)-50\r\n    );\r\n\r\n    const shippers=[\r\n        {\r\n            description:\"General Cargo \u2014 In Gauge\",\r\n            length:Math.min(e.length-400,2200),\r\n            width:Math.min(openTopExampleWidthLimit-80,1800),\r\n            height:inGaugeHeight,\r\n            weight:250,\r\n            color:SHIPPER_COLORS[0]\r\n        },\r\n        {\r\n            description:\"Machine Crate \u2014 OOG Overheight\",\r\n            length:Math.min(e.length-3000,1800),\r\n            width:Math.min(openTopExampleWidthLimit-40,1900),\r\n            height:e.wallHeight+650,\r\n            weight:420,\r\n            color:SHIPPER_COLORS[0]\r\n        },\r\n        {\r\n            description:\"Equipment Crate \u2014 OOG Overheight\",\r\n            length:Math.min(e.length-4200,1400),\r\n            width:Math.min(openTopExampleWidthLimit-20,2100),\r\n            height:e.wallHeight+1100,\r\n            weight:500,\r\n            color:SHIPPER_COLORS[0]\r\n        }\r\n    ];\r\n\r\n    \/* One shipper only, three different cargo dimensions. One remains\r\n       in-gauge; two deliberately exceed the side-wall height. *\/\r\n    shippers.forEach(function(item,index){\r\n        createCargoRow({\r\n            shipper:\"Shipper A\",\r\n            description:item.description,\r\n            length:item.length,\r\n            width:item.width,\r\n            height:item.height,\r\n            qty:1,\r\n            weight:item.weight,\r\n            rotation:\"no-rotate\",\r\n            stackable:false,\r\n            rollable:false,\r\n            maxStack:1,\r\n            color:item.color\r\n        });\r\n\r\n        const row=cargoRows[cargoRows.length-1];\r\n\r\n        const x=index===0 ? 350 : index===1 ? 2750 : 4300;\r\n        const z=(e.width-item.width)\/2;\r\n\r\n        exampleBlueprint.push({\r\n            order:index,\r\n            section:0,\r\n            layer:0,\r\n            x:Math.max(0,Math.min(x,e.length-item.length)),\r\n            y:0,\r\n            z:Math.max(0,z),\r\n            l:item.length,\r\n            w:item.width,\r\n            h:item.height,\r\n            rowId:row.id,\r\n            shipper:\"Shipper A\",\r\n            shipperIndex:0\r\n        });\r\n    });\r\n\r\n    byId(\"nscMainAlert\").className=\"nsc-lp-alert success\";\r\n    byId(\"nscMainAlert\").textContent=\r\n        `${e.label} Open Top example loaded. One shipper, three cargo sizes: one IN GAUGE and two OUT GAUGE \/ overheight pieces. The OOG pieces are allowed above the Open Top side-wall height because the roof is open and the example represents top-loading.`;\r\n\r\n    updateEquipmentRuleUI();\r\n    specialRenderCargoTable();\r\n}\r\n\r\nfunction loadFlatRackExample(){\r\n    clearCargoRows();\r\n    exampleMode=true;\r\n    exampleBlueprint=[];\r\n\r\n    const e=currentEquipment();\r\n\r\n    \/* Keep the OOG cargo visually clear of both end frames while still\r\n       occupying almost the entire usable platform length. *\/\r\n    const endClearance=NSC_FLAT_RACK_END_CLEARANCE;\r\n    const usableLength=Math.min(\r\n        e.maxCargoLength||e.length,\r\n        e.length\r\n    );\r\n\r\n    const cargoLength=Math.max(\r\n        usableLength-(endClearance*2),\r\n        1000\r\n    );\r\n\r\n    const cargoWidth=e.width+750;\r\n    const cargoHeight=e.height+750;\r\n\r\n    \/* Center the single Flat Rack example piece\r\n       between the back and front end walls. *\/\r\n    const centerX=(e.length-cargoLength)\/2;\r\n    const centerZ=(e.width-cargoWidth)\/2;\r\n\r\n    createCargoRow({\r\n        shipper:\"Shipper A\",\r\n        description:\"Project Cargo \u2014 OOG Flat Rack\",\r\n        length:cargoLength,\r\n        width:cargoWidth,\r\n        height:cargoHeight,\r\n        qty:1,\r\n        weight:6500,\r\n        rotation:\"no-rotate\",\r\n        stackable:false,\r\n        rollable:false,\r\n        maxStack:1,\r\n        color:SHIPPER_COLORS[0]\r\n    });\r\n\r\n    const row=cargoRows[cargoRows.length-1];\r\n\r\n    exampleBlueprint.push({\r\n        order:0,\r\n        section:0,\r\n        layer:0,\r\n        x:centerX,\r\n        y:0,\r\n        z:centerZ,\r\n        l:cargoLength,\r\n        w:cargoWidth,\r\n        h:cargoHeight,\r\n        rowId:row.id,\r\n        shipper:\"Shipper A\",\r\n        shipperIndex:0\r\n    });\r\n\r\n    byId(\"nscMainAlert\").className=\"nsc-lp-alert success\";\r\n    byId(\"nscMainAlert\").textContent=\r\n        `${e.label} Flat Rack example loaded. One project-cargo piece runs almost the full platform length while keeping ${fmt(endClearance,0)} mm visual clearance from both end frames. Its width and height intentionally exceed the normal frame profile, so it is shown as OUT GAUGE \/ OOG.`;\r\n\r\n    updateEquipmentRuleUI();\r\n    specialRenderCargoTable();\r\n}\r\n\r\nfunction loadDryExample(){\r\n    return nscV23.loadExample();\r\n}\r\n\r\nfunction specialLoadExample(){\r\n    const e=currentEquipment();\r\n    const family=equipmentFamily(e);\r\n\r\n    if(family===\"open-top\"){\r\n        loadOpenTopExample();\r\n        return;\r\n    }\r\n\r\n    if(family===\"flat-rack\"){\r\n        loadFlatRackExample();\r\n        return;\r\n    }\r\n\r\n    loadDryExample();\r\n}\r\n\r\n\/* =========================================================\r\n   SPECIAL EXAMPLE PLACEMENT PLAN\r\n========================================================= *\/\r\nfunction isSpecialExample(){\r\n    return exampleMode &&\r\n        Array.isArray(exampleBlueprint) &&\r\n        exampleBlueprint.length>0 &&\r\n        !isDryEquipment(currentEquipment());\r\n}\r\n\r\nfunction planSpecialExample(pieces,e,gap){\r\n    const placed=[];\r\n    const unplaced=[];\r\n    const pools=new Map();\r\n\r\n    pieces.forEach(function(piece){\r\n        if(!pools.has(piece.rowId))pools.set(piece.rowId,[]);\r\n        pools.get(piece.rowId).push(piece);\r\n    });\r\n\r\n    exampleBlueprint.forEach(function(cell){\r\n        const pool=pools.get(cell.rowId);\r\n        const piece=pool&&pool.length?pool.shift():null;\r\n\r\n        if(!piece)return;\r\n\r\n        const test={\r\n            x:cell.x,\r\n            y:cell.y,\r\n            z:cell.z,\r\n            l:cell.l,\r\n            w:cell.w,\r\n            h:cell.h\r\n        };\r\n\r\n        if(!specialPlacementAllowed(test,placed,e,\"section\",gap)){\r\n            unplaced.push(piece);\r\n            return;\r\n        }\r\n\r\n        piece.placed=true;\r\n        piece.exampleSection=1;\r\n        piece.exampleLayer=1;\r\n        piece.exampleOrder=cell.order+1;\r\n        piece.x=cell.x;\r\n        piece.y=cell.y;\r\n        piece.z=cell.z;\r\n        piece.l=cell.l;\r\n        piece.w=cell.w;\r\n        piece.h=cell.h;\r\n        placed.push(piece);\r\n    });\r\n\r\n    pools.forEach(function(pool){\r\n        pool.forEach(function(piece){\r\n            unplaced.push(piece);\r\n        });\r\n    });\r\n\r\n    const usedVolume=placed.reduce((s,p)=>s+p.l*p.w*p.h\/1e9,0);\r\n    const placedWeight=placed.reduce((s,p)=>s+p.weight,0);\r\n\r\n    \/* For special equipment, \"volume utilization\" is not a reliable\r\n       decision metric when cargo is intentionally OOG. Keep it as 0 and\r\n       expose the gauge state through warnings\/report instead. *\/\r\n    const balance=calculateBalance(placed,e);\r\n    const warnings=specialBuildWarnings(e,placed,unplaced,usedVolume,placedWeight,balance);\r\n\r\n    return {\r\n        equipment:e,\r\n        pieces:pieces,\r\n        placed:placed,\r\n        unplaced:unplaced,\r\n        usedVolume:usedVolume,\r\n        placedWeight:placedWeight,\r\n        utilization:e.capacity\r\n            ? Math.min(100,(usedVolume\/e.capacity)*100)\r\n            : 0,\r\n        balance:balance,\r\n        warnings:warnings,\r\n        exampleFullCapacity:false,\r\n        exampleSpecialEquipment:true\r\n    };\r\n}\r\n\r\n\/* =========================================================\r\n   PLAN LOAD OVERRIDE\r\n========================================================= *\/\r\nfunction specialPlanLoad(){\r\n    const e=currentEquipment();\r\n\r\n    if(isSpecialExample()){\r\n        const gap=Math.max(0,safeNum(byId(\"nscGapTolerance\").value));\r\n        return planSpecialExample(expandCargo(),e,gap);\r\n    }\r\n\r\n    if(isDryEquipment(e)){\r\n        return nscV23.planLoad();\r\n    }\r\n\r\n    \/* Normal user cargo for Open Top \/ Flat Rack uses the new\r\n       equipment-specific placement rules. *\/\r\n    const mode=byId(\"nscPackMode\").value;\r\n    const gap=Math.max(0,safeNum(byId(\"nscGapTolerance\").value));\r\n    const pieces=expandCargo();\r\n\r\n    const placed=[];\r\n    const unplaced=[];\r\n    const totalRequestedWeight=getTotalRequestedWeight();\r\n\r\n    for(const piece of pieces){\r\n        const rotations=rotationsFor(piece);\r\n        let best=null;\r\n        let bestScore=Infinity;\r\n\r\n        for(const rot of rotations){\r\n            const positions=specialCandidatePositions(piece,rot,placed,e,mode,gap);\r\n\r\n            for(const pos of positions){\r\n                if(!specialPlacementAllowed(pos,placed,e,mode,gap))continue;\r\n\r\n                const gauge=gaugeForPiece({\r\n                    l:pos.l,\r\n                    w:pos.w,\r\n                    h:pos.h\r\n                },e,gap);\r\n\r\n                \/* Prefer in-gauge positions when available. *\/\r\n                const gaugePenalty=gauge.status===\"IN GAUGE\" ? 0 : gauge.status===\"OUT GAUGE\" ? 5000 : 20000;\r\n\r\n                \/*\r\n                   Special equipment uses centered placement as a\r\n                   deterministic preference. This prevents a color,\r\n                   description, size, quantity or handling edit from\r\n                   pushing cargo back to x=0 on the next plan run.\r\n                   GP \/ 40GP \/ 40HC are not affected because those\r\n                   return through nscV23 above.\r\n                *\/\r\n                const centerOffsetX=Math.abs((pos.x+pos.l\/2)-e.length\/2);\r\n                const centerOffsetZ=Math.abs((pos.z+pos.w\/2)-e.width\/2);\r\n\r\n                const score=\r\n                    gaugePenalty +\r\n                    (pos.y*10) +\r\n                    (centerOffsetX*.8) +\r\n                    (centerOffsetZ*.18);\r\n\r\n                if(score<bestScore){\r\n                    bestScore=score;\r\n                    best=pos;\r\n                }\r\n            }\r\n        }\r\n\r\n        if(best){\r\n            piece.placed=true;\r\n            piece.x=best.x;\r\n            piece.y=best.y;\r\n            piece.z=best.z;\r\n            piece.l=best.l;\r\n            piece.w=best.w;\r\n            piece.h=best.h;\r\n            placed.push(piece);\r\n        }else{\r\n            unplaced.push(piece);\r\n        }\r\n    }\r\n\r\n    const usedVolume=placed.reduce((s,p)=>s+p.l*p.w*p.h\/1e9,0);\r\n    const placedWeight=placed.reduce((s,p)=>s+p.weight,0);\r\n    const balance=calculateBalance(placed,e);\r\n    const warnings=specialBuildWarnings(e,placed,unplaced,usedVolume,placedWeight,balance);\r\n\r\n    return {\r\n        equipment:e,\r\n        pieces:pieces,\r\n        placed:placed,\r\n        unplaced:unplaced,\r\n        usedVolume:usedVolume,\r\n        placedWeight:placedWeight,\r\n        utilization:e.capacity?usedVolume\/e.capacity*100:0,\r\n        balance:balance,\r\n        warnings:warnings\r\n    };\r\n}\r\n\r\n\/* =========================================================\r\n   SPECIAL WARNINGS\r\n========================================================= *\/\r\nfunction specialBuildWarnings(e,placed,unplaced,usedVolume,placedWeight,balance){\r\n    const warnings=[];\r\n\r\n    if(unplaced.length){\r\n        warnings.push(\r\n            `${unplaced.length} cargo piece(s) could not be placed with the current equipment-specific rules, dimensions and constraints.`\r\n        );\r\n    }\r\n\r\n    if(placedWeight>e.payload){\r\n        warnings.push(\r\n            `Planned placed cargo weight ${fmt(placedWeight,2)} kg exceeds the equipment payload reference of ${fmt(e.payload,0)} kg.`\r\n        );\r\n    }\r\n\r\n    if(isDryEquipment(e) && e.capacity && usedVolume>e.capacity && !exampleMode){\r\n        warnings.push(\r\n            \"Calculated placed geometric volume exceeds the nominal container volume.\"\r\n        );\r\n    }\r\n\r\n    if(balance&&Math.abs(balance.ratio)>.10){\r\n        warnings.push(\r\n            `Estimated longitudinal center of gravity is offset from the container midpoint (${fmt(balance.delta\/1000,2)} m). Review weight distribution and securing.`\r\n        );\r\n    }\r\n\r\n    placed.forEach(function(p){\r\n        const gauge=gaugeForPiece(p,e,Math.max(0,safeNum(byId(\"nscGapTolerance\").value)));\r\n\r\n        if(gauge.status===\"OUT GAUGE\"){\r\n            warnings.push(\r\n                `${p.description} (${p.shipper}) is OUT GAUGE: ${gauge.reason}`\r\n            );\r\n        }\r\n\r\n        if(gauge.status===\"BLOCKED\"){\r\n            warnings.push(\r\n                `${p.description} (${p.shipper}) cannot be accepted by the current equipment envelope: ${gauge.reason}`\r\n            );\r\n        }\r\n\r\n        if(p.rollable===true){\r\n            warnings.push(\r\n                `${p.description} (${p.shipper}) is marked rollable. Review chocking, blocking, lashing or other securing requirements before actual stuffing.`\r\n            );\r\n        }\r\n\r\n        if(p.stackable===false){\r\n            warnings.push(\r\n                `${p.description} (${p.shipper}) is marked non-stackable. The planner will not use it as a support surface for another cargo piece.`\r\n            );\r\n        }\r\n    });\r\n\r\n    return warnings;\r\n}\r\n\r\n\/* =========================================================\r\n   SPECIAL CONTAINER RENDERING \u2014 THREE.JS\r\n========================================================= *\/\r\nfunction specialRenderContainer(){\r\n    const e=currentEquipment();\r\n\r\n    if(isDryEquipment(e)){\r\n        nscV23.renderContainer();\r\n        return;\r\n    }\r\n\r\n    if(!scene||!containerGroup)return;\r\n\r\n    clearGroup(containerGroup);\r\n\r\n    const L=e.length\/1000;\r\n    const W=e.width\/1000;\r\n    const H=e.height\/1000;\r\n\r\n    const floorMat=new THREE.MeshStandardMaterial({\r\n        color:0x2f80c0,\r\n        roughness:.78,\r\n        metalness:.05\r\n    });\r\n\r\n    const floor=new THREE.Mesh(\r\n        new THREE.BoxGeometry(L,.08,W),\r\n        floorMat\r\n    );\r\n    floor.position.y=.04;\r\n    containerGroup.add(floor);\r\n\r\n    const frameMat=new THREE.MeshStandardMaterial({\r\n        color:0x687684,\r\n        roughness:.72,\r\n        metalness:.18\r\n    });\r\n\r\n    const backMat=new THREE.MeshStandardMaterial({\r\n        color:0x0b4f8a,\r\n        roughness:.75,\r\n        metalness:.08\r\n    });\r\n\r\n    const frontMat=new THREE.MeshStandardMaterial({\r\n        color:0xf59e0b,\r\n        roughness:.62,\r\n        metalness:.08\r\n    });\r\n\r\n    const wallMat=new THREE.MeshBasicMaterial({\r\n        color:0x9aa8b6,\r\n        transparent:true,\r\n        opacity:.08,\r\n        side:THREE.DoubleSide,\r\n        depthWrite:false\r\n    });\r\n\r\n    function beam(x,y,z,bx,by,bz,mat){\r\n        const m=new THREE.Mesh(\r\n            new THREE.BoxGeometry(bx,by,bz),\r\n            mat||frameMat\r\n        );\r\n        m.position.set(x,y,z);\r\n        containerGroup.add(m);\r\n        return m;\r\n    }\r\n\r\n    const t=.075;\r\n    const post=.105;\r\n\r\n    if(isOpenTopEquipment(e)){\r\n        \/* Four vertical corner posts + bottom perimeter + side walls.\r\n           No roof surface. *\/\r\n        [[-L\/2,-W\/2],[L\/2,-W\/2],[-L\/2,W\/2],[L\/2,W\/2]].forEach(function(pair){\r\n            const mat=pair[0]>0?frontMat:backMat;\r\n            beam(pair[0],H\/2,pair[1],post,H,post,mat);\r\n        });\r\n\r\n        beam(0,t\/2,-W\/2,L,t,t,frameMat);\r\n        beam(0,t\/2,W\/2,L,t,t,frameMat);\r\n        beam(-L\/2,t\/2,0,t,t,W,frameMat);\r\n        beam(L\/2,t\/2,0,t,t,W,frontMat);\r\n\r\n        beam(0,H-t\/2,-W\/2,L,t,t,frameMat);\r\n        beam(0,H-t\/2,W\/2,L,t,t,frameMat);\r\n\r\n        \/* Side wall panels stop at wall height. *\/\r\n        const sideLeft=new THREE.Mesh(\r\n            new THREE.BoxGeometry(L,H-.10,.035),\r\n            wallMat\r\n        );\r\n        sideLeft.position.set(0,H\/2,-W\/2+.018);\r\n        containerGroup.add(sideLeft);\r\n\r\n        const sideRight=sideLeft.clone();\r\n        sideRight.position.z=W\/2-.018;\r\n        containerGroup.add(sideRight);\r\n\r\n        \/* Back wall marker and front door marker. *\/\r\n        const backPanel=new THREE.Mesh(\r\n            new THREE.BoxGeometry(.035,H,W-.04),\r\n            new THREE.MeshBasicMaterial({\r\n                color:0x0b4f8a,\r\n                transparent:true,\r\n                opacity:.055,\r\n                side:THREE.DoubleSide,\r\n                depthWrite:false\r\n            })\r\n        );\r\n        backPanel.position.set(-L\/2+.018,H\/2,0);\r\n        containerGroup.add(backPanel);\r\n\r\n        const doorPanel=new THREE.Mesh(\r\n            new THREE.BoxGeometry(.035,H,W-.04),\r\n            new THREE.MeshBasicMaterial({\r\n                color:0xf59e0b,\r\n                transparent:true,\r\n                opacity:.07,\r\n                side:THREE.DoubleSide,\r\n                depthWrite:false\r\n            })\r\n        );\r\n        doorPanel.position.set(L\/2-.018,H\/2,0);\r\n        containerGroup.add(doorPanel);\r\n\r\n        \/* Roof-opening rectangle \/ removable-top indication. *\/\r\n        const goldMat=new THREE.MeshStandardMaterial({\r\n            color:0xc9a227,\r\n            roughness:.65,\r\n            metalness:.12\r\n        });\r\n        beam(0,H-.03,-W\/2,L,.055,.055,goldMat);\r\n        beam(0,H-.03,W\/2,L,.055,.055,goldMat);\r\n        beam(-L\/2,H-.03,0,.055,.055,W,goldMat);\r\n        beam(L\/2,H-.03,0,.055,.055,W,goldMat);\r\n\r\n    }else{\r\n        \/* Flat Rack:\r\n           - floor\/platform\r\n           - no door\r\n           - structural front and back end walls\r\n           - bright corner posts + horizontal end-wall rails\r\n           - open left\/right\/top *\/\r\n        const endPost=.12;\r\n        const endBeam=.105;\r\n        const endWallMatBack=new THREE.MeshStandardMaterial({\r\n            color:0x0b4f8a,\r\n            roughness:.82,\r\n            metalness:.12,\r\n            transparent:true,\r\n            opacity:.16,\r\n            side:THREE.DoubleSide,\r\n            depthWrite:false\r\n        });\r\n        const endWallMatFront=new THREE.MeshStandardMaterial({\r\n            color:0xf59e0b,\r\n            roughness:.78,\r\n            metalness:.10,\r\n            transparent:true,\r\n            opacity:.16,\r\n            side:THREE.DoubleSide,\r\n            depthWrite:false\r\n        });\r\n\r\n        \/* Structural end-wall planes are visible, but translucent so OOG cargo remains readable. *\/\r\n        const backWall=new THREE.Mesh(\r\n            new THREE.BoxGeometry(.035,H,W-.12),\r\n            endWallMatBack\r\n        );\r\n        backWall.position.set(-L\/2+.018,H\/2,0);\r\n        containerGroup.add(backWall);\r\n\r\n        const frontWall=new THREE.Mesh(\r\n            new THREE.BoxGeometry(.035,H,W-.12),\r\n            endWallMatFront\r\n        );\r\n        frontWall.position.set(L\/2-.018,H\/2,0);\r\n        containerGroup.add(frontWall);\r\n\r\n        \/* Four bright corner posts. *\/\r\n        beam(-L\/2,H\/2,-W\/2,endPost,H,endPost,backMat);\r\n        beam(-L\/2,H\/2,W\/2,endPost,H,endPost,backMat);\r\n        beam(L\/2,H\/2,-W\/2,endPost,H,endPost,frontMat);\r\n        beam(L\/2,H\/2,W\/2,endPost,H,endPost,frontMat);\r\n\r\n        \/* End-wall top, bottom and intermediate rails. *\/\r\n        [0.08,.30,.54,.76,.96].forEach(function(frac){\r\n            const y=Math.max(.06,Math.min(H-.06,H*frac));\r\n            beam(-L\/2,y,0,endBeam,.085,W-.08,backMat);\r\n            beam(L\/2,y,0,endBeam,.085,W-.08,frontMat);\r\n        });\r\n\r\n        \/* Left\/right sides and roof remain completely open. *\/\r\n        beam(0,.08,-W\/2,L,.10,.06,frameMat);\r\n        beam(0,.08,W\/2,L,.10,.06,frameMat);\r\n\r\n        \/* Visual ground-to-platform separation supports. *\/\r\n        const supportMat=new THREE.MeshStandardMaterial({\r\n            color:0x475569,\r\n            roughness:.80,\r\n            metalness:.10\r\n        });\r\n        const supportY=-(NSC_GROUND_CLEARANCE\/2);\r\n        const supportH=NSC_GROUND_CLEARANCE;\r\n        [[-L*.34,-W*.38],[L*.34,-W*.38],[-L*.34,W*.38],[L*.34,W*.38]].forEach(function(pair){\r\n            beam(pair[0],supportY,pair[1],.34,supportH,.28,supportMat);\r\n        });\r\n\r\n        \/* Gauge frame markers. *\/\r\n        const gold=new THREE.MeshStandardMaterial({\r\n            color:0xc9a227,\r\n            roughness:.65,\r\n            metalness:.12\r\n        });\r\n        beam(0,H, -W\/2, L,.035,.035,gold);\r\n        beam(0,H, W\/2, L,.035,.035,gold);\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   SPECIAL CONTAINER RENDERING \u2014 CANVAS FALLBACK\r\n========================================================= *\/\r\nfunction specialDrawContainerFallback(ctx,e,sceneInfo){\r\n    if(isDryEquipment(e)){\r\n        nscV23.drawContainerFallback(ctx,e,sceneInfo);\r\n        return;\r\n    }\r\n\r\n    const L=e.length\/1000;\r\n    const W=e.width\/1000;\r\n    const H=e.height\/1000;\r\n\r\n    \/* Floor. *\/\r\n    const floor=[\r\n        {x:-L\/2,y:0,z:-W\/2},\r\n        {x:L\/2,y:0,z:-W\/2},\r\n        {x:L\/2,y:0,z:W\/2},\r\n        {x:-L\/2,y:0,z:W\/2}\r\n    ].map(pt=>fallbackProject(pt,sceneInfo));\r\n    drawPoly(\r\n        ctx,\r\n        floor,\r\n        \"rgba(47,128,192,.92)\",\r\n        \"rgba(201,162,39,.72)\",\r\n        .98\r\n    );\r\n\r\n    if(isOpenTopEquipment(e)){\r\n        \/* Side wall panels. Draw before cargo so cargo remains visible. *\/\r\n        const left=[\r\n            {x:-L\/2,y:0,z:-W\/2},\r\n            {x:L\/2,y:0,z:-W\/2},\r\n            {x:L\/2,y:H,z:-W\/2},\r\n            {x:-L\/2,y:H,z:-W\/2}\r\n        ].map(pt=>fallbackProject(pt,sceneInfo));\r\n        drawPoly(ctx,left,\"rgba(135,151,166,.13)\",\"rgba(83,98,115,.65)\",.72);\r\n\r\n        const right=[\r\n            {x:-L\/2,y:0,z:W\/2},\r\n            {x:L\/2,y:0,z:W\/2},\r\n            {x:L\/2,y:H,z:W\/2},\r\n            {x:-L\/2,y:H,z:W\/2}\r\n        ].map(pt=>fallbackProject(pt,sceneInfo));\r\n        drawPoly(ctx,right,\"rgba(135,151,166,.13)\",\"rgba(83,98,115,.65)\",.72);\r\n\r\n        \/* Back \/ inside wall. *\/\r\n        const back=[\r\n            {x:-L\/2,y:0,z:-W\/2},\r\n            {x:-L\/2,y:0,z:W\/2},\r\n            {x:-L\/2,y:H,z:W\/2},\r\n            {x:-L\/2,y:H,z:-W\/2}\r\n        ].map(pt=>fallbackProject(pt,sceneInfo));\r\n        drawPoly(ctx,back,\"rgba(11,79,138,.055)\",\"rgba(11,79,138,.86)\",.72);\r\n\r\n        \/* Front \/ door. *\/\r\n        const front=[\r\n            {x:L\/2,y:0,z:-W\/2},\r\n            {x:L\/2,y:0,z:W\/2},\r\n            {x:L\/2,y:H,z:W\/2},\r\n            {x:L\/2,y:H,z:-W\/2}\r\n        ].map(pt=>fallbackProject(pt,sceneInfo));\r\n        drawPoly(ctx,front,\"rgba(245,158,11,.055)\",\"rgba(245,158,11,.95)\",.72);\r\n\r\n        \/* Open-top perimeter only. *\/\r\n        drawLine3D(ctx,[\r\n            {x:-L\/2,y:H,z:-W\/2},\r\n            {x:L\/2,y:H,z:-W\/2},\r\n            {x:L\/2,y:H,z:W\/2},\r\n            {x:-L\/2,y:H,z:W\/2},\r\n            {x:-L\/2,y:H,z:-W\/2}\r\n        ],sceneInfo,\"#c9a227\",1.3,.86,[5,4]);\r\n\r\n    }else{\r\n        \/* Flat Rack: structural end walls at front\/back; side and roof open. *\/\r\n        const backWall=[\r\n            {x:-L\/2,y:0,z:-W\/2},\r\n            {x:-L\/2,y:0,z:W\/2},\r\n            {x:-L\/2,y:H,z:W\/2},\r\n            {x:-L\/2,y:H,z:-W\/2}\r\n        ].map(pt=>fallbackProject(pt,sceneInfo));\r\n        drawPoly(ctx,backWall,\"rgba(11,79,138,.14)\",\"rgba(11,79,138,.98)\",.88);\r\n\r\n        const frontWall=[\r\n            {x:L\/2,y:0,z:-W\/2},\r\n            {x:L\/2,y:0,z:W\/2},\r\n            {x:L\/2,y:H,z:W\/2},\r\n            {x:L\/2,y:H,z:-W\/2}\r\n        ].map(pt=>fallbackProject(pt,sceneInfo));\r\n        drawPoly(ctx,frontWall,\"rgba(245,158,11,.14)\",\"rgba(245,158,11,.98)\",.88);\r\n\r\n        const edges=[\r\n            [{x:-L\/2,y:0,z:-W\/2},{x:-L\/2,y:H,z:-W\/2}],\r\n            [{x:-L\/2,y:0,z:W\/2},{x:-L\/2,y:H,z:W\/2}],\r\n            [{x:L\/2,y:0,z:-W\/2},{x:L\/2,y:H,z:-W\/2}],\r\n            [{x:L\/2,y:0,z:W\/2},{x:L\/2,y:H,z:W\/2}],\r\n            [{x:-L\/2,y:H,z:-W\/2},{x:-L\/2,y:H,z:W\/2}],\r\n            [{x:L\/2,y:H,z:-W\/2},{x:L\/2,y:H,z:W\/2}],\r\n            [{x:-L\/2,y:0,z:-W\/2},{x:-L\/2,y:0,z:W\/2}],\r\n            [{x:L\/2,y:0,z:-W\/2},{x:L\/2,y:0,z:W\/2}]\r\n        ];\r\n        edges.forEach(function(edge,index){\r\n            drawLine3D(\r\n                ctx,\r\n                edge,\r\n                sceneInfo,\r\n                index<2 || index===6 ? \"#0b4f8a\" : \"#f59e0b\",\r\n                2.15,\r\n                .92\r\n            );\r\n        });\r\n\r\n        \/* End-wall reinforcing rails. *\/\r\n        [0.08,.30,.54,.76,.96].forEach(function(frac){\r\n            const y=Math.max(.06,Math.min(H-.06,H*frac));\r\n            drawLine3D(ctx,[\r\n                {x:-L\/2,y:y,z:-W\/2},\r\n                {x:-L\/2,y:y,z:W\/2}\r\n            ],sceneInfo,\"#0b4f8a\",1.35,.84);\r\n            drawLine3D(ctx,[\r\n                {x:L\/2,y:y,z:-W\/2},\r\n                {x:L\/2,y:y,z:W\/2}\r\n            ],sceneInfo,\"#f59e0b\",1.35,.84);\r\n        });\r\n\r\n        \/* Longitudinal platform rails \u2014 left\/right remain open. *\/\r\n        drawLine3D(ctx,[\r\n            {x:-L\/2,y:0,z:-W\/2},\r\n            {x:L\/2,y:0,z:-W\/2}\r\n        ],sceneInfo,\"#536273\",1.1,.8);\r\n\r\n        drawLine3D(ctx,[\r\n            {x:-L\/2,y:0,z:W\/2},\r\n            {x:L\/2,y:0,z:W\/2}\r\n        ],sceneInfo,\"#536273\",1.1,.8);\r\n\r\n        \/* Frame-profile gauge lines. *\/\r\n        drawLine3D(ctx,[\r\n            {x:-L\/2,y:H,z:-W\/2},\r\n            {x:L\/2,y:H,z:-W\/2}\r\n        ],sceneInfo,\"#c9a227\",1.1,.72,[4,4]);\r\n\r\n        drawLine3D(ctx,[\r\n            {x:-L\/2,y:H,z:W\/2},\r\n            {x:L\/2,y:H,z:W\/2}\r\n        ],sceneInfo,\"#c9a227\",1.1,.72,[4,4]);\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   FALLBACK CARGO BORDER FOR OOG\r\n========================================================= *\/\r\nfunction specialRenderFallbackScene(plan){\r\n    if(renderMode!==\"canvas3d\"||!fallbackCtx||!fallbackCanvas)return;\r\n\r\n    const activePlan=(plan===undefined)?currentPlan:plan;\r\n\r\n    \/* Use original stable canvas renderer, but temporarily ensure it uses the\r\n       equipment-specific container drawing and cargo data. *\/\r\n    nscV23.renderFallbackScene(activePlan);\r\n\r\n    if(!activePlan)return;\r\n\r\n    \/* Add explicit OOG outlines after the original cargo pass. *\/\r\n    if(isDryEquipment(activePlan.equipment))return;\r\n\r\n    const size=getCanvasSize(fallbackCanvas);\r\n    const ctx=fallbackCtx;\r\n    const e=activePlan.equipment;\r\n    const sceneMax=Math.max(e.length,e.width,specialBoundsFor(e).H)\/1000;\r\n    const scale=Math.min(\r\n        size.w\/(sceneMax*1.42),\r\n        size.h\/(sceneMax*1.22)\r\n    )*fallbackCamera.zoom;\r\n\r\n    const sceneInfo={\r\n        cx:size.w\/2,\r\n        cy:size.h*.64,\r\n        scale:scale,\r\n        maxDim:sceneMax\r\n    };\r\n\r\n    activePlan.placed.forEach(function(p){\r\n        const gauge=gaugeForPiece(p,e,Math.max(0,safeNum(byId(\"nscGapTolerance\").value)));\r\n        if(gauge.status!==\"OUT GAUGE\")return;\r\n\r\n        const box={\r\n            x:(p.x-e.length\/2)\/1000,\r\n            y:p.y\/1000,\r\n            z:(p.z-e.width\/2)\/1000,\r\n            l:p.l\/1000,\r\n            w:p.w\/1000,\r\n            h:p.h\/1000\r\n        };\r\n\r\n        const pts=[\r\n            {x:box.x,y:box.y,z:box.z},\r\n            {x:box.x+box.l,y:box.y,z:box.z},\r\n            {x:box.x+box.l,y:box.y+box.h,z:box.z},\r\n            {x:box.x,y:box.y+box.h,z:box.z}\r\n        ];\r\n        drawLine3D(ctx,pts,sceneInfo,\"#c83d3d\",2.2,.95);\r\n    });\r\n}\r\n\r\n\/* =========================================================\r\n   THREE.JS PLAN RENDER OVERRIDE\r\n========================================================= *\/\r\nfunction specialRenderPlan(plan){\r\n    nscV23.renderPlan(plan);\r\n\r\n    if(!plan)return;\r\n\r\n    if(renderMode===\"three\" && cargoGroup){\r\n        \/* Re-color cargo edge outlines by gauge state. *\/\r\n        cargoGroup.children.forEach(function(mesh){\r\n            if(!mesh.userData)return;\r\n            const p=plan.placed.find(function(item){return item.uid===mesh.userData.uid;});\r\n            if(!p)return;\r\n\r\n            const gauge=gaugeForPiece(p,plan.equipment,Math.max(0,safeNum(byId(\"nscGapTolerance\").value)));\r\n\r\n            mesh.traverse(function(node){\r\n                if(node!==mesh && node.material && node.material.color){\r\n                    node.material.color.set(\r\n                        gauge.status===\"OUT GAUGE\"\r\n                            ? 0xc83d3d\r\n                            : gauge.status===\"BLOCKED\"\r\n                                ? 0xb53026\r\n                                : 0x172b3a\r\n                    );\r\n                }\r\n            });\r\n        });\r\n\r\n        if(renderer&&scene&&camera){\r\n            renderer.render(scene,camera);\r\n        }\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   REPORT OVERRIDE\r\n========================================================= *\/\r\nfunction specialRenderReport(plan){\r\n    nscV23.renderReport(plan);\r\n\r\n    const handling=byId(\"nscHandlingSummary\");\r\n    if(handling && plan){\r\n        const inGauge=plan.placed.filter(function(p){\r\n            return gaugeForPiece(p,plan.equipment,Math.max(0,safeNum(byId(\"nscGapTolerance\").value))).status===\"IN GAUGE\";\r\n        }).length;\r\n\r\n        const outGauge=plan.placed.filter(function(p){\r\n            return gaugeForPiece(p,plan.equipment,Math.max(0,safeNum(byId(\"nscGapTolerance\").value))).status===\"OUT GAUGE\";\r\n        }).length;\r\n\r\n        const blocked=plan.unplaced.length;\r\n\r\n        const family=equipmentFamily(plan.equipment);\r\n        const note=family===\"open-top\"\r\n            ? \"Open Top: cargo above side-wall height is OUT GAUGE \/ overheight and assumes top-loading.\"\r\n            : family===\"flat-rack\"\r\n                ? \"Flat Rack: length is platform-limited; width and height may exceed the frame profile as OUT GAUGE.\"\r\n                : \"20GP \/ 40GP \/ 40HC: standard enclosed-container section\/layer rules apply.\";\r\n\r\n        handling.innerHTML=\r\n            `<strong>${fmt(inGauge,0)}<\/strong> in gauge \u00b7 ` +\r\n            `<strong>${fmt(outGauge,0)}<\/strong> out gauge \u00b7 ` +\r\n            `<strong>${fmt(blocked,0)}<\/strong> not placed<br>` +\r\n            `<span class=\"nsc-lp-special-rule\"><strong>Equipment rule:<\/strong> ${escapeHtml(note)}<\/span>`;\r\n    }\r\n}\r\n\r\n\/* =========================================================\r\n   RUN-TIME INITIALIZATION\r\n========================================================= *\/\r\nspecialUpdateEquipmentUI();\r\nspecialRenderCargoTable();\r\n\r\n\/* =========================================================\r\n   SPECIAL EQUIPMENT CHANGE \/ EXAMPLE REFRESH\r\n========================================================= *\/\r\nbyId(\"nscContainerType\").removeEventListener &&\r\n    byId(\"nscContainerType\").addEventListener(\"change\",function(){\r\n        window.setTimeout(function(){\r\n            specialUpdateEquipmentUI();\r\n            specialRenderCargoTable();\r\n        },0);\r\n    });\r\n\r\n\r\n\/* =========================================================\r\n   EVENT WIRING\r\n========================================================= *\/\r\nbyId(\"nscContainerType\").addEventListener(\"change\",function(){\r\n    exampleMode=false;\r\n    exampleBlueprint=null;\r\n    currentPlan=null;\r\n    specialUpdateEquipmentUI();\r\n    reset3DView();\r\n    if(renderMode===\"three\")specialRenderContainer();\r\n    else if(renderMode===\"canvas3d\")specialRenderFallbackScene(null);\r\n    resetReport();\r\n});\r\n\r\nbyId(\"nscAddCargo\").addEventListener(\"click\",function(){\r\n    exampleMode=false;\r\n    exampleBlueprint=null;\r\n    createCargoRow();\r\n});\r\n\r\nbyId(\"nscLoadExample\").addEventListener(\"click\",function(){\r\n    specialLoadExample();\r\n});\r\n\r\nbyId(\"nscResetPlanner\").addEventListener(\"click\",function(){\r\n    resetPlanner();\r\n});\r\n\r\nbyId(\"nscViewReset\").addEventListener(\"click\",function(){\r\n    reset3DView();\r\n});\r\n\r\nbyId(\"nscView360\").addEventListener(\"click\",function(){\r\n    toggle360View();\r\n});\r\n\r\nbyId(\"nscRunPlanner\").addEventListener(\"click\",function(){\r\n    if(!cargoRows.length){\r\n        byId(\"nscMainAlert\").className=\"nsc-lp-alert error\";\r\n        byId(\"nscMainAlert\").textContent=\"Please add at least one cargo item before creating the plan.\";\r\n        return;\r\n    }\r\n\r\n    const invalid=cargoRows.some(r=>r.length<=0||r.width<=0||r.height<=0||r.qty<=0||r.weight<0);\r\n    if(invalid){\r\n        byId(\"nscMainAlert\").className=\"nsc-lp-alert error\";\r\n        byId(\"nscMainAlert\").textContent=\"Please check the cargo size, number of pieces and weight per piece.\";\r\n        return;\r\n    }\r\n\r\n    const plan=specialPlanLoad();\r\n    specialRenderPlan(plan);\r\n    specialRenderReport(plan);\r\n});\r\n\r\n\/* =========================================================\r\n   STARTUP\r\n========================================================= *\/\r\ncreateCargoRow();\r\nspecialUpdateEquipmentUI();\r\nresetReport();\r\nsetViewControlState(false);\r\ninit3D();\r\nif(renderMode===\"canvas3d\")specialRenderFallbackScene();\r\nelse if(renderMode===\"three\")resize3D();\r\n\r\n})();\r\n<\/script>   \t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>NS CONTINENT 3D LOGISTICS TOOL 3D Load Planner See how cargo from one or more shippers could fit inside a container. Choose the container, enter your cargo details in cm, then create a 3D loading plan to see what fits and where it goes. MULTI-SHIPPER CM INPUT 3D VIEW WEIGHT AWARE 20 \/ 40 \/ [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"pagelayer_contact_templates":[],"_pagelayer_content":"","footnotes":""},"class_list":["post-1504","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/pages\/1504","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/comments?post=1504"}],"version-history":[{"count":10,"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/pages\/1504\/revisions"}],"predecessor-version":[{"id":1518,"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/pages\/1504\/revisions\/1518"}],"wp:attachment":[{"href":"https:\/\/nscontinent.com\/id\/wp-json\/wp\/v2\/media?parent=1504"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}