{"id":604,"date":"2026-01-14T20:09:15","date_gmt":"2026-01-15T00:09:15","guid":{"rendered":"https:\/\/hiropaper.ca\/?page_id=604"},"modified":"2026-02-02T18:00:37","modified_gmt":"2026-02-02T22:00:37","slug":"card-ordering","status":"publish","type":"page","link":"https:\/\/hiropaper.ca\/?page_id=604","title":{"rendered":"Card Ordering"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><strong>Hiro Paper Card Printing Service<\/strong><\/h2>\n\n\n\n<p>Welcome to our card printing service.<\/p>\n\n\n\n<p>From here you can upload a custom XML file that points to specific card art and back art found on public Google drives. Certain websites allow you to choose specific art and then save the information into an XML file. Another option is to upload your own custom art files onto your own Google drive, set the folder access to &#8220;Anyone with the link &#8211; Viewer&#8221;, copy the share link, and go to our <a href=\"https:\/\/hiropaper.ca\/?page_id=653\" target=\"_blank\" rel=\"noopener\" title=\"\"><strong><span style=\"text-decoration: underline;\">XML tool<\/span><\/strong><\/a> paste the link and follow the steps.<\/p>\n\n\n\n<p>Watch our video on using this page: <a href=\"https:\/\/www.youtube.com\/watch?v=j6iY75ncFdc\">TCG Playing Card Printing Service on Feels Like Magic 320GSM by Hiro Paper<\/a><\/p>\n\n\n\t\t<div class=\"hiro-mpcfill-wrap\" data-rest=\"https:\/\/hiropaper.ca\/index.php?rest_route=\/hiro-mpcfill\/v1\" data-nonce=\"0ded72e926\">\r\n\t\t\t<div class=\"hiro-mpcfill-header\">\r\n\t\t\t\t<h2 class=\"hiro-title\">Print from XML<\/h2>\r\n\t\t\t\t<p class=\"hiro-subtitle\">Upload your XML to preview your sheets and add printing to cart.<\/p>\r\n\t\t\t<\/div>\r\n\r\n\t\t\t<div class=\"hiro-card\">\r\n\t\t\t\t<label class=\"hiro-label\">Upload XML<\/label>\r\n\t\t\t\t<input type=\"file\" id=\"hiroXmlFile\" accept=\".xml,text\/xml,application\/xml\" \/>\r\n\t\t\t\t<div class=\"hiro-hint\">Tip: If images don\u2019t load, your Google Drive files may be private. Set sharing to \u201cAnyone with the link\u201d or share the folder with us.<\/div>\r\n\t\t\t<\/div>\r\n\r\n\t\t\t<div id=\"hiroSummary\" class=\"hiro-card\" style=\"display:none;\">\r\n\t\t\t\t<div class=\"hiro-summary-grid\">\r\n\t\t\t\t\t<div><div class=\"hiro-k\">Total cards<\/div><div class=\"hiro-v\" id=\"hiroTotalCards\">\u2014<\/div><\/div>\r\n\t\t\t\t\t<div><div class=\"hiro-k\">Sheets required<\/div><div class=\"hiro-v\" id=\"hiroSheetsNeeded\">\u2014<\/div><\/div>\r\n\t\t\t\t\t<div><div class=\"hiro-k\">Missing art<\/div><div class=\"hiro-v\" id=\"hiroMissingCards\">\u2014<\/div><\/div>\r\n\t\t\t\t\t<div><div class=\"hiro-k\">Sheets billed<\/div><div class=\"hiro-v\" id=\"hiroBillableSheets\">\u2014<\/div><\/div>\r\n\t\t\t\t<\/div>\r\n\t\t\t\t<div class=\"hiro-warn\" id=\"hiroWarn\" style=\"display:none;\"><\/div>\r\n\t\t\t<\/div>\r\n\r\n\t\t\t<div id=\"hiroPager\" class=\"hiro-card\" style=\"display:none;\">\r\n\t\t\t\t<div class=\"hiro-pager\">\r\n\t\t\t\t\t<button class=\"hiro-btn hiro-btn-secondary\" id=\"hiroPrev\">Prev<\/button>\r\n\t\t\t\t\t<div class=\"hiro-pageinfo\"><span id=\"hiroPageLabel\">Sheet 1 \/ 1<\/span><\/div>\r\n\t\t\t\t\t<button class=\"hiro-btn hiro-btn-secondary\" id=\"hiroNext\">Next<\/button>\r\n\t\t\t\t<\/div>\r\n\t\t\t<\/div>\r\n\r\n\t\t\t<div id=\"hiroGridWrap\" class=\"hiro-card\" style=\"display:none;\">\r\n\t\t\t\t<div class=\"hiro-grid-title\">Proof<\/div>\r\n\r\n\t\t\t\t<!-- \u2705 Headers -->\r\n\t\t\t\t<div class=\"hiro-grid-head\">\r\n\t\t\t\t\t<div class=\"hiro-grid-head-cell\">FRONT<\/div>\r\n\t\t\t\t\t<div class=\"hiro-grid-head-cell\">BACK<\/div>\r\n\t\t\t\t<\/div>\r\n\r\n\t\t\t\t<div class=\"hiro-grid-4x4\" id=\"hiroGrid\"><\/div>\r\n\t\t\t<\/div>\r\n\r\n\t\t\t<div id=\"hiroActions\" class=\"hiro-card\" style=\"display:none;\">\r\n\t\t\t\t<button class=\"hiro-btn hiro-btn-primary\" id=\"hiroAddToCart\">Add to cart<\/button>\r\n\t\t\t<\/div>\r\n\r\n\t\t\t<!-- Modal 1: Missing art -->\r\n\t\t\t<div class=\"hiro-modal-backdrop\" id=\"hiroModalBackdrop\" style=\"display:none;\">\r\n\t\t\t\t<div class=\"hiro-modal\">\r\n\t\t\t\t\t<div class=\"hiro-modal-title\">Missing card art detected<\/div>\r\n\t\t\t\t\t<div class=\"hiro-modal-text\">\r\n\t\t\t\t\t\tSome of your card art is missing (fronts and\/or backs), we may not be able to print these. Would you like to continue?\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"hiro-modal-actions\">\r\n\t\t\t\t\t\t<button class=\"hiro-btn hiro-btn-secondary\" id=\"hiroModalNo\">No<\/button>\r\n\t\t\t\t\t\t<button class=\"hiro-btn hiro-btn-primary\" id=\"hiroModalYes\">Yes<\/button>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t<\/div>\r\n\t\t\t<\/div>\r\n\r\n\t\t\t<!-- Modal 2: Existing print order in cart -->\r\n\t\t\t<div class=\"hiro-modal-backdrop\" id=\"hiroCartChoiceBackdrop\" style=\"display:none;\">\r\n\t\t\t\t<div class=\"hiro-modal\">\r\n\t\t\t\t\t<div class=\"hiro-modal-title\">Print order already in cart<\/div>\r\n\t\t\t\t\t<div class=\"hiro-modal-text\">\r\n\t\t\t\t\t\tYou already have a print order in your cart. Would you like to add this as a second print order or replace the first?\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t\t<div class=\"hiro-modal-actions\">\r\n\t\t\t\t\t\t<button class=\"hiro-btn hiro-btn-secondary\" id=\"hiroChoiceCancel\">Cancel<\/button>\r\n\t\t\t\t\t\t<button class=\"hiro-btn hiro-btn-secondary\" id=\"hiroChoiceReplace\">Replace<\/button>\r\n\t\t\t\t\t\t<button class=\"hiro-btn hiro-btn-primary\" id=\"hiroChoiceAdd\">Add as second<\/button>\r\n\t\t\t\t\t<\/div>\r\n\t\t\t\t<\/div>\r\n\t\t\t<\/div>\r\n\t\t<\/div>\r\n\r\n\t\t<style>\r\n\t\t\tbody { min-height: 100dvh }\r\n\t\t\t.hiro-mpcfill-wrap{max-width:980px;margin:24px auto;padding:0 12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}\r\n\t\t\t.hiro-title{margin:0 0 6px 0;font-size:26px}\r\n\t\t\t.hiro-subtitle{margin:0 0 18px 0;opacity:.85}\r\n\t\t\t.hiro-card{background:#1118270d;border:1px solid #1118271f;border-radius:14px;padding:14px;margin:12px 0}\r\n\t\t\t.hiro-label{display:block;font-weight:600;margin-bottom:8px}\r\n\t\t\t.hiro-hint{margin-top:10px;font-size:13px;opacity:.8}\r\n\t\t\t.hiro-summary-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}\r\n\t\t\t.hiro-k{font-size:12px;opacity:.7}\r\n\t\t\t.hiro-v{font-size:18px;font-weight:700}\r\n\t\t\t.hiro-warn{margin-top:10px;padding:10px 12px;border-radius:12px;background:#f59e0b1a;border:1px solid #f59e0b55}\r\n\t\t\t.hiro-pager{display:flex;align-items:center;justify-content:space-between;gap:12px}\r\n\t\t\t.hiro-pageinfo{font-weight:700}\r\n\t\t\t.hiro-btn{border-radius:12px;padding:10px 14px;border:1px solid #11182733;background:#fff;cursor:pointer;font-weight:700}\r\n\t\t\t.hiro-btn-primary{background:#111827;color:#fff;border-color:#111827}\r\n\t\t\t.hiro-btn-secondary{background:#fff;color:#111827}\r\n\t\t\t.hiro-grid-title{font-weight:800;margin-bottom:10px}\r\n\r\n\t\t\t\/* \u2705 FRONT\/BACK header row *\/\r\n\t\t\t.hiro-grid-head{\r\n\t\t\t\tdisplay:grid;\r\n\t\t\t\tgrid-template-columns: 1fr 1fr;\r\n\t\t\t\tgap:10px;\r\n\t\t\t\tmargin-bottom:10px;\r\n\t\t\t\tposition:relative;\r\n\t\t\t}\r\n\t\t\t.hiro-grid-head-cell{\r\n\t\t\t\ttext-align:center;\r\n\t\t\t\tfont-weight:900;\r\n\t\t\t\tletter-spacing:.18em;\r\n\t\t\t\topacity:.85;\r\n\t\t\t\tpadding:6px 0;\r\n\t\t\t}\r\n\t\t\t.hiro-grid-head::after{\r\n\t\t\t\tcontent:\"\";\r\n\t\t\t\tposition:absolute;\r\n\t\t\t\ttop:8px;\r\n\t\t\t\tbottom:8px;\r\n\t\t\t\tleft:50%;\r\n\t\t\t\twidth:2px;\r\n\t\t\t\ttransform:translateX(-1px);\r\n\t\t\t\tbackground:rgba(17,24,39,.22);\r\n\t\t\t\tborder-radius:2px;\r\n\t\t\t}\r\n\r\n\t\t\t.hiro-grid-4x4{\r\n\t\t\t\tdisplay:grid;\r\n\t\t\t\tgrid-template-columns:repeat(4, 1fr);\r\n\t\t\t\tgrid-template-rows:repeat(4, auto);\r\n\t\t\t\tgap:10px;\r\n\t\t\t\tposition:relative;\r\n\t\t\t}\r\n\t\t\t\/* \u2705 Vertical separator line between col 2 and 3 (front vs back halves) *\/\r\n\t\t\t.hiro-grid-4x4::after{\r\n\t\t\t\tcontent:\"\";\r\n\t\t\t\tposition:absolute;\r\n\t\t\t\ttop:0;\r\n\t\t\t\tbottom:0;\r\n\t\t\t\tleft:50%;\r\n\t\t\t\twidth:2px;\r\n\t\t\t\ttransform:translateX(-1px);\r\n\t\t\t\tbackground:rgba(17,24,39,.22);\r\n\t\t\t\tborder-radius:2px;\r\n\t\t\t\tpointer-events:none;\r\n\t\t\t}\r\n\r\n\t\t\t.hiro-tile{\r\n\t\t\t\tposition:relative;\r\n\t\t\t\taspect-ratio: 63 \/ 88;\r\n\t\t\t\tborder-radius:12px;\r\n\t\t\t\tborder:1px solid #11182733;\r\n\t\t\t\toverflow:hidden;\r\n\t\t\t\tbackground:#0b1220;\r\n\t\t\t\tdisplay:flex;\r\n\t\t\t\talign-items:center;\r\n\t\t\t\tjustify-content:center;\r\n\t\t\t\tcolor:#fff;\r\n\t\t\t\ttext-align:center;\r\n\t\t\t\tpadding:8px;\r\n\t\t\t\tfont-size:12px;\r\n\t\t\t}\r\n\t\t\t.hiro-tile img{width:100%;height:100%;object-fit:cover;display:block}\r\n\t\t\t.hiro-tile .hiro-missing{line-height:1.25}\r\n\t\t\t.hiro-tile .hiro-missing strong{display:block;margin-bottom:6px}\r\n\t\t\t.hiro-tile .hiro-empty{opacity:.65}\r\n\r\n\t\t\t\/* \u2705 DFC badge *\/\r\n\t\t\t.hiro-badge{\r\n\t\t\t\tposition:absolute;\r\n\t\t\t\ttop:8px;\r\n\t\t\t\tleft:8px;\r\n\t\t\t\tpadding:4px 8px;\r\n\t\t\t\tborder-radius:999px;\r\n\t\t\t\tfont-weight:900;\r\n\t\t\t\tfont-size:11px;\r\n\t\t\t\tletter-spacing:.08em;\r\n\t\t\t\tbackground:rgba(0,0,0,.55);\r\n\t\t\t\tborder:1px solid rgba(255,255,255,.18);\r\n\t\t\t\tbackdrop-filter: blur(4px);\r\n\t\t\t\tpointer-events:none;\r\n\t\t\t}\r\n\r\n\t\t\t\/* Modal *\/\r\n\t\t\t.hiro-modal-backdrop{\r\n\t\t\t\tposition:fixed;inset:0;background:rgba(0,0,0,.55);\r\n\t\t\t\tdisplay:flex;align-items:center;justify-content:center;z-index:9999;\r\n\t\t\t\tpadding:16px;\r\n\t\t\t}\r\n\t\t\t.hiro-modal{max-width:520px;width:100%;background:#0A161D;border-radius:16px;padding:16px;border:1px solid #11182722}\r\n\t\t\t.hiro-modal-title{font-size:18px;font-weight:900;margin-bottom:8px}\r\n\t\t\t.hiro-modal-text{opacity:.85;margin-bottom:14px}\r\n\t\t\t.hiro-modal-actions{display:flex;justify-content:flex-end;gap:10px;flex-wrap:wrap}\r\n\t\t<\/style>\r\n\r\n\t\t<script>\r\n\t\t(function(){\r\n\t\t\tconst wrap = document.querySelector('.hiro-mpcfill-wrap');\r\n\t\t\tif (!wrap) return;\r\n\r\n\t\t\tconst REST_BASE = wrap.dataset.rest;\r\n\t\t\tconst NONCE = wrap.dataset.nonce;\r\n\r\n\t\t\tconst fileInput = document.getElementById('hiroXmlFile');\r\n\t\t\tconst summaryBox = document.getElementById('hiroSummary');\r\n\t\t\tconst pagerBox = document.getElementById('hiroPager');\r\n\t\t\tconst gridWrap = document.getElementById('hiroGridWrap');\r\n\t\t\tconst actionsBox = document.getElementById('hiroActions');\r\n\r\n\t\t\tconst totalCardsEl = document.getElementById('hiroTotalCards');\r\n\t\t\tconst sheetsNeededEl = document.getElementById('hiroSheetsNeeded');\r\n\t\t\tconst missingCardsEl = document.getElementById('hiroMissingCards');\r\n\t\t\tconst billableSheetsEl = document.getElementById('hiroBillableSheets');\r\n\t\t\tconst warnEl = document.getElementById('hiroWarn');\r\n\r\n\t\t\tconst prevBtn = document.getElementById('hiroPrev');\r\n\t\t\tconst nextBtn = document.getElementById('hiroNext');\r\n\t\t\tconst pageLabel = document.getElementById('hiroPageLabel');\r\n\t\t\tconst grid = document.getElementById('hiroGrid');\r\n\r\n\t\t\tconst addBtn = document.getElementById('hiroAddToCart');\r\n\r\n\t\t\tconst modalMissing = document.getElementById('hiroModalBackdrop');\r\n\t\t\tconst modalNo = document.getElementById('hiroModalNo');\r\n\t\t\tconst modalYes = document.getElementById('hiroModalYes');\r\n\r\n\t\t\tconst modalChoice = document.getElementById('hiroCartChoiceBackdrop');\r\n\t\t\tconst choiceCancel = document.getElementById('hiroChoiceCancel');\r\n\t\t\tconst choiceReplace = document.getElementById('hiroChoiceReplace');\r\n\t\t\tconst choiceAdd = document.getElementById('hiroChoiceAdd');\r\n\r\n\t\t\tlet xmlText = '';\r\n\t\t\tlet cards = []; \/\/ ordered by slot\r\n\t\t\tlet cardbackId = '';\r\n\t\t\tlet backsBySlot = new Map(); \/\/ slot -> { id, name }\r\n\t\t\tlet totalCards = 0;\r\n\t\t\tlet sheetsNeeded = 0;\r\n\t\t\tlet currentSheet = 0;\r\n\r\n\t\t\tlet missingFrontSlots = new Set(); \/\/ globalIndex\r\n\t\t\tlet missingBackSlots = new Set();  \/\/ globalIndex\r\n\t\t\tlet missingCards = 0;\r\n\t\t\tlet billableSheets = 0;\r\n\r\n\t\t\tfunction unionMissingCount(){\r\n\t\t\t\tconst u = new Set();\r\n\t\t\t\tfor (const x of missingFrontSlots) u.add(x);\r\n\t\t\t\tfor (const x of missingBackSlots) u.add(x);\r\n\t\t\t\treturn u.size;\r\n\t\t\t}\r\n\r\n\t\t\tfunction recomputeMissingAndBillable(){\r\n\t\t\t\tmissingCards = unionMissingCount();\r\n\t\t\t\tconst missingSheets = Math.floor(missingCards \/ 8);\r\n\t\t\t\tbillableSheets = Math.max(1, sheetsNeeded - missingSheets);\r\n\t\t\t}\r\n\r\n\t\t\tfunction driveThumbUrl(fileId){\r\n\t\t\t\treturn `https:\/\/drive.google.com\/thumbnail?id=${encodeURIComponent(fileId)}&sz=w1000`;\r\n\t\t\t}\r\n\r\n\t\t\tfunction setWarn(text){\r\n\t\t\t\tif (!text){\r\n\t\t\t\t\twarnEl.style.display='none';\r\n\t\t\t\t\twarnEl.textContent='';\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\t\t\t\twarnEl.style.display='block';\r\n\t\t\t\twarnEl.textContent=text;\r\n\t\t\t}\r\n\r\n\t\t\tfunction updateSummary(){\r\n\t\t\t\ttotalCardsEl.textContent = String(totalCards);\r\n\t\t\t\tsheetsNeededEl.textContent = String(sheetsNeeded);\r\n\t\t\t\tmissingCardsEl.textContent = String(missingCards);\r\n\t\t\t\tbillableSheetsEl.textContent = String(billableSheets);\r\n\r\n\t\t\t\tif (missingCards > 0){\r\n\t\t\t\t\tsetWarn('Some card art could not be loaded (fronts and\/or backs). This is usually caused by Google Drive sharing settings (private\/removed).');\r\n\t\t\t\t} else {\r\n\t\t\t\t\tsetWarn('');\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tfunction parseSlots(slotsRaw){\r\n\t\t\t\treturn (slotsRaw || '')\r\n\t\t\t\t\t.trim()\r\n\t\t\t\t\t.split(\/[,\\s]+\/)\r\n\t\t\t\t\t.filter(Boolean)\r\n\t\t\t\t\t.map(s => Number(s))\r\n\t\t\t\t\t.filter(n => Number.isFinite(n));\r\n\t\t\t}\r\n\r\n\t\t\tfunction parseXml(text){\r\n\t\t\t\tconst doc = new DOMParser().parseFromString(text, \"application\/xml\");\r\n\t\t\t\tconst parseError = doc.querySelector(\"parsererror\");\r\n\t\t\t\tif (parseError) throw new Error(\"XML parse error. Make sure this is a valid MPCFill XML file.\");\r\n\r\n\t\t\t\tconst cardNodes = [...doc.querySelectorAll(\"order > fronts > card\")];\r\n\t\t\t\tconst out = [];\r\n\r\n\t\t\t\tfor (const node of cardNodes){\r\n\t\t\t\t\tconst id = (node.querySelector(\"id\")?.textContent || \"\").trim();\r\n\t\t\t\t\tconst name = (node.querySelector(\"name\")?.textContent || \"\").trim();\r\n\t\t\t\t\tconst slotsRaw = (node.querySelector(\"slots\")?.textContent || \"\").trim();\r\n\t\t\t\t\tconst slots = parseSlots(slotsRaw);\r\n\r\n\t\t\t\t\tfor (const slot of slots){\r\n\t\t\t\t\t\tout.push({ slot, id, name });\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tout.sort((a,b) => a.slot - b.slot);\r\n\r\n\t\t\t\tcardbackId = (doc.querySelector(\"order > cardback\")?.textContent || \"\").trim();\r\n\r\n\t\t\t\tbacksBySlot = new Map();\r\n\t\t\t\tconst backNodes = [...doc.querySelectorAll(\"order > backs > card\")];\r\n\t\t\t\tfor (const node of backNodes){\r\n\t\t\t\t\tconst id = (node.querySelector(\"id\")?.textContent || \"\").trim();\r\n\t\t\t\t\tconst name = (node.querySelector(\"name\")?.textContent || \"\").trim();\r\n\t\t\t\t\tconst slotsRaw = (node.querySelector(\"slots\")?.textContent || \"\").trim();\r\n\t\t\t\t\tconst slots = parseSlots(slotsRaw);\r\n\t\t\t\t\tfor (const slot of slots){\r\n\t\t\t\t\t\tbacksBySlot.set(slot, { id, name: name || 'Back' });\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn out;\r\n\t\t\t}\r\n\r\n\t\t\tfunction tileMissing(name){\r\n\t\t\t\tconst div = document.createElement('div');\r\n\t\t\t\tdiv.className='hiro-tile';\r\n\t\t\t\tconst inner = document.createElement('div');\r\n\t\t\t\tinner.className='hiro-missing';\r\n\t\t\t\tinner.innerHTML = `<strong>Could not load card art<\/strong>${escapeHtml(name || 'Unknown file')}`;\r\n\t\t\t\tdiv.appendChild(inner);\r\n\t\t\t\treturn div;\r\n\t\t\t}\r\n\r\n\t\t\tfunction tileEmpty(label){\r\n\t\t\t\tconst div = document.createElement('div');\r\n\t\t\t\tdiv.className='hiro-tile';\r\n\t\t\t\tconst inner = document.createElement('div');\r\n\t\t\t\tinner.className='hiro-empty';\r\n\t\t\t\tinner.textContent = label || 'Empty';\r\n\t\t\t\tdiv.appendChild(inner);\r\n\t\t\t\treturn div;\r\n\t\t\t}\r\n\r\n\t\t\tfunction tileImage(src, alt, onError, badgeText){\r\n\t\t\t\tconst div = document.createElement('div');\r\n\t\t\t\tdiv.className='hiro-tile';\r\n\r\n\t\t\t\tif (badgeText){\r\n\t\t\t\t\tconst badge = document.createElement('div');\r\n\t\t\t\t\tbadge.className = 'hiro-badge';\r\n\t\t\t\t\tbadge.textContent = badgeText;\r\n\t\t\t\t\tdiv.appendChild(badge);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tconst img = document.createElement('img');\r\n\t\t\t\timg.src = src;\r\n\t\t\t\timg.alt = alt || '';\r\n\t\t\t\timg.onerror = onError;\r\n\t\t\t\tdiv.appendChild(img);\r\n\t\t\t\treturn div;\r\n\t\t\t}\r\n\r\n\t\t\tfunction escapeHtml(s){\r\n\t\t\t\treturn String(s).replace(\/[&<>\"']\/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[m]));\r\n\t\t\t}\r\n\r\n\t\t\tfunction renderSheet(){\r\n\t\t\t\tgrid.innerHTML = '';\r\n\r\n\t\t\t\tpageLabel.textContent = `Sheet ${currentSheet + 1} \/ ${Math.max(1, sheetsNeeded)}`;\r\n\t\t\t\tprevBtn.disabled = currentSheet <= 0;\r\n\t\t\t\tnextBtn.disabled = currentSheet >= sheetsNeeded - 1;\r\n\r\n\t\t\t\tconst start = currentSheet * 8;\r\n\t\t\t\tconst cells = new Array(16).fill(null);\r\n\r\n\t\t\t\tfor (let i = 0; i < 8; i++){\r\n\t\t\t\t\tconst globalIndex = start + i;\r\n\t\t\t\t\tconst row = Math.floor(i \/ 2);\r\n\t\t\t\t\tconst col = i % 2;\r\n\r\n\t\t\t\t\tconst frontPos = row * 4 + col;\r\n\t\t\t\t\tconst backPos  = row * 4 + (col+2);\r\n\r\n\t\t\t\t\tconst card = cards[globalIndex];\r\n\r\n\t\t\t\t\t\/\/ Front\r\n\t\t\t\t\tif (!card){\r\n\t\t\t\t\t\tcells[frontPos] = tileEmpty('Empty');\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tif (missingFrontSlots.has(globalIndex)){\r\n\t\t\t\t\t\t\tcells[frontPos] = tileMissing(card.name);\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tcells[frontPos] = tileImage(\r\n\t\t\t\t\t\t\t\tdriveThumbUrl(card.id),\r\n\t\t\t\t\t\t\t\tcard.name,\r\n\t\t\t\t\t\t\t\t() => {\r\n\t\t\t\t\t\t\t\t\tif (!missingFrontSlots.has(globalIndex)){\r\n\t\t\t\t\t\t\t\t\t\tmissingFrontSlots.add(globalIndex);\r\n\t\t\t\t\t\t\t\t\t\trecomputeMissingAndBillable();\r\n\t\t\t\t\t\t\t\t\t\tupdateSummary();\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\tconst replacement = tileMissing(card.name);\r\n\t\t\t\t\t\t\t\t\tcells[frontPos].replaceWith(replacement);\r\n\t\t\t\t\t\t\t\t\tcells[frontPos] = replacement;\r\n\t\t\t\t\t\t\t\t},\r\n\t\t\t\t\t\t\t\tnull\r\n\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\/\/ Back per-slot fallback to default\r\n\t\t\t\t\tif (!card){\r\n\t\t\t\t\t\tcells[backPos] = tileEmpty('Empty');\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tlet back = null;\r\n\t\t\t\t\t\tconst isDfc = backsBySlot && backsBySlot.has(card.slot);\r\n\t\t\t\t\t\tif (isDfc) back = backsBySlot.get(card.slot);\r\n\r\n\t\t\t\t\t\tconst backId = (back && back.id) ? back.id : cardbackId;\r\n\t\t\t\t\t\tconst backName = (back && back.name) ? back.name : 'Card back';\r\n\t\t\t\t\t\tconst badge = isDfc ? 'DFC' : '';\r\n\r\n\t\t\t\t\t\tif (!backId){\r\n\t\t\t\t\t\t\tcells[backPos] = tileEmpty('No back ID');\r\n\t\t\t\t\t\t\tif (!missingBackSlots.has(globalIndex)){\r\n\t\t\t\t\t\t\t\tmissingBackSlots.add(globalIndex);\r\n\t\t\t\t\t\t\t\trecomputeMissingAndBillable();\r\n\t\t\t\t\t\t\t\tupdateSummary();\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t} else if (missingBackSlots.has(globalIndex)){\r\n\t\t\t\t\t\t\tcells[backPos] = tileEmpty('Back not accessible');\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tcells[backPos] = tileImage(\r\n\t\t\t\t\t\t\t\tdriveThumbUrl(backId),\r\n\t\t\t\t\t\t\t\tbackName,\r\n\t\t\t\t\t\t\t\t() => {\r\n\t\t\t\t\t\t\t\t\tif (!missingBackSlots.has(globalIndex)){\r\n\t\t\t\t\t\t\t\t\t\tmissingBackSlots.add(globalIndex);\r\n\t\t\t\t\t\t\t\t\t\trecomputeMissingAndBillable();\r\n\t\t\t\t\t\t\t\t\t\tupdateSummary();\r\n\t\t\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t\t\t\tconst replacement = tileEmpty('Back not accessible');\r\n\t\t\t\t\t\t\t\t\tcells[backPos].replaceWith(replacement);\r\n\t\t\t\t\t\t\t\t\tcells[backPos] = replacement;\r\n\t\t\t\t\t\t\t\t},\r\n\t\t\t\t\t\t\t\tbadge\r\n\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tfor (const cell of cells){\r\n\t\t\t\t\tgrid.appendChild(cell || tileEmpty(''));\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tasync function restPost(path, body){\r\n\t\t\t\tconst res = await fetch(`${REST_BASE}${path}`, {\r\n\t\t\t\t\tmethod: 'POST',\r\n\t\t\t\t\theaders: {\r\n\t\t\t\t\t\t'Content-Type':'application\/json',\r\n\t\t\t\t\t\t'X-WP-Nonce': NONCE\r\n\t\t\t\t\t},\r\n\t\t\t\t\tbody: JSON.stringify(body)\r\n\t\t\t\t});\r\n\t\t\t\tconst data = await res.json().catch(()=> ({}));\r\n\t\t\t\tif (!res.ok) throw new Error(data.error || 'Request failed');\r\n\t\t\t\treturn data;\r\n\t\t\t}\r\n\r\n\t\t\tfunction showMissingModal(){ modalMissing.style.display='flex'; }\r\n\t\t\tfunction hideMissingModal(){ modalMissing.style.display='none'; }\r\n\r\n\t\t\tfunction showChoiceModal(){ modalChoice.style.display='flex'; }\r\n\t\t\tfunction hideChoiceModal(){ modalChoice.style.display='none'; }\r\n\r\n\t\t\tfileInput.addEventListener('change', async (e) => {\r\n\t\t\t\tconst f = e.target.files && e.target.files[0];\r\n\t\t\t\tif (!f) return;\r\n\r\n\t\t\t\ttry{\r\n\t\t\t\t\txmlText = await f.text();\r\n\t\t\t\t\tcards = parseXml(xmlText);\r\n\t\t\t\t\ttotalCards = cards.length;\r\n\t\t\t\t\tsheetsNeeded = totalCards > 0 ? Math.ceil(totalCards \/ 8) : 0;\r\n\t\t\t\t\tcurrentSheet = 0;\r\n\r\n\t\t\t\t\tmissingFrontSlots = new Set();\r\n\t\t\t\t\tmissingBackSlots = new Set();\r\n\t\t\t\t\tmissingCards = 0;\r\n\r\n\t\t\t\t\trecomputeMissingAndBillable();\r\n\r\n\t\t\t\t\tif (totalCards <= 0){\r\n\t\t\t\t\t\tthrow new Error('No cards found in XML (no slot entries).');\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tsummaryBox.style.display = '';\r\n\t\t\t\t\tpagerBox.style.display = '';\r\n\t\t\t\t\tgridWrap.style.display = '';\r\n\t\t\t\t\tactionsBox.style.display = '';\r\n\r\n\t\t\t\t\tupdateSummary();\r\n\t\t\t\t\trenderSheet();\r\n\r\n\t\t\t\t} catch(err){\r\n\t\t\t\t\talert(err.message || String(err));\r\n\t\t\t\t\tsummaryBox.style.display='none';\r\n\t\t\t\t\tpagerBox.style.display='none';\r\n\t\t\t\t\tgridWrap.style.display='none';\r\n\t\t\t\t\tactionsBox.style.display='none';\r\n\t\t\t\t}\r\n\t\t\t});\r\n\r\n\t\t\tprevBtn.addEventListener('click', () => {\r\n\t\t\t\tif (currentSheet > 0){\r\n\t\t\t\t\tcurrentSheet--;\r\n\t\t\t\t\trenderSheet();\r\n\t\t\t\t}\r\n\t\t\t});\r\n\r\n\t\t\tnextBtn.addEventListener('click', () => {\r\n\t\t\t\tif (currentSheet < sheetsNeeded - 1){\r\n\t\t\t\t\tcurrentSheet++;\r\n\t\t\t\t\trenderSheet();\r\n\t\t\t\t}\r\n\t\t\t});\r\n\r\n\t\t\taddBtn.addEventListener('click', async () => {\r\n\t\t\t\tif (!xmlText) return;\r\n\r\n\t\t\t\tif (missingCards > 0){\r\n\t\t\t\t\tshowMissingModal();\r\n\t\t\t\t} else {\r\n\t\t\t\t\tawait proceedAddToCart();\r\n\t\t\t\t}\r\n\t\t\t});\r\n\r\n\t\t\tmodalNo.addEventListener('click', () => hideMissingModal());\r\n\t\t\tmodalYes.addEventListener('click', async () => {\r\n\t\t\t\thideMissingModal();\r\n\t\t\t\tawait proceedAddToCart();\r\n\t\t\t});\r\n\r\n\t\t\tchoiceCancel.addEventListener('click', () => hideChoiceModal());\r\n\t\t\tchoiceReplace.addEventListener('click', async () => {\r\n\t\t\t\thideChoiceModal();\r\n\t\t\t\tawait proceedAddToCart('replace');\r\n\t\t\t});\r\n\t\t\tchoiceAdd.addEventListener('click', async () => {\r\n\t\t\t\thideChoiceModal();\r\n\t\t\t\tawait proceedAddToCart('add');\r\n\t\t\t});\r\n\r\n\t\t\tasync function proceedAddToCart(mode){\r\n\t\t\t\taddBtn.disabled = true;\r\n\t\t\t\taddBtn.textContent = 'Adding...';\r\n\r\n\t\t\t\ttry{\r\n\t\t\t\t\tconst job = await restPost('\/create_job', {\r\n\t\t\t\t\t\txml: xmlText,\r\n\t\t\t\t\t\tmissing_cards: missingCards\r\n\t\t\t\t\t});\r\n\r\n\t\t\t\t\tconst payload = {\r\n\t\t\t\t\t\tjob_id: job.job_id,\r\n\t\t\t\t\t\ttoken: job.token\r\n\t\t\t\t\t};\r\n\t\t\t\t\tif (mode) payload.mode = mode;\r\n\r\n\t\t\t\t\tconst out = await restPost('\/add_to_cart', payload);\r\n\r\n\t\t\t\t\tif (out.needs_choice){\r\n\t\t\t\t\t\tshowChoiceModal();\r\n\t\t\t\t\t\taddBtn.disabled = false;\r\n\t\t\t\t\t\taddBtn.textContent = 'Add to cart';\r\n\t\t\t\t\t\treturn;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (out.redirect) {\r\n\t\t\t\t\t\twindow.location.href = out.redirect;\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\twindow.location.href = 'https:\/\/hiropaper.ca\/?page_id=8';\r\n\t\t\t\t\t}\r\n\t\t\t\t} catch(err){\r\n\t\t\t\t\talert(err.message || String(err));\r\n\t\t\t\t\taddBtn.disabled = false;\r\n\t\t\t\t\taddBtn.textContent = 'Add to cart';\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t})();\r\n\t\t<\/script>\r\n\t\t\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hiro Paper Card Printing Service Welcome to our card printing service. From here you can upload a custom XML file&#8230;<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-604","page","type-page","status-publish","hentry"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/604","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=604"}],"version-history":[{"count":5,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/604\/revisions"}],"predecessor-version":[{"id":688,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/604\/revisions\/688"}],"wp:attachment":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=604"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}