{"id":277,"date":"2025-08-31T15:39:00","date_gmt":"2025-08-31T19:39:00","guid":{"rendered":"https:\/\/hiropaper.ca\/?page_id=277"},"modified":"2026-02-05T21:18:12","modified_gmt":"2026-02-06T01:18:12","slug":"tcg-proxy-sheet-generator","status":"publish","type":"page","link":"https:\/\/hiropaper.ca\/?page_id=277","title":{"rendered":"TCG Proxy Sheet Generator"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Hiro Paper \u2013 TCG Print Tools<\/title>\n  <style>\n    :root{\n      --bg:#0f3c57;\n      --bg-light:#135476;\n      --border:#ffffff;\n      --text:#ffffff;\n      --muted:#ffffffcc;\n      --accent:#00d0ff;\n    }\n    *{box-sizing:border-box;}\n    body{\n      font-family:sans-serif;\n      min-height: 100vh;\n      text-align:center;\n      margin:0;\n      background:var(--bg);\n      color:var(--text);\n    }\n    h1,h2,h3,p,label,span,div,li,ul{color:var(--text);}\n    a{color:var(--accent);}\n\n    \/* Top tabs *\/\n    #tabs{\n      position:sticky;\n      top:0;\n      z-index:5000;\n\n      border-bottom:0px solid var(--border);\n      padding:10px 10px;\n      display:flex;\n      gap:8px;\n      justify-content:center;\n      flex-wrap:wrap;\n    }\n    .tabBtn{\n      padding:10px 14px;\n      font-size:15px;\n      cursor:pointer;\n      background:var(--bg-light);\n      color:var(--text);\n      border:1px solid var(--border);\n      border-radius:8px;\n      opacity:0.85;\n    }\n    .tabBtn.active{\n      opacity:1;\n      border-color:var(--accent);\n      box-shadow:0 0 0 2px rgba(0,208,255,0.25);\n    }\n\n    \/* Shared controls *\/\n    #controls{\n      margin:10px auto 10px auto;\n      display:flex;\n      flex-wrap:wrap;\n      align-items:center;\n      justify-content:center;\n      gap:8px;\n      max-width:900px;\n      padding:0 10px;\n    }\n\n    button{\n      padding:10px 20px;\n      font-size:16px;\n      cursor:pointer;\n      background:var(--bg-light);\n      color:var(--text);\n      border:1px solid var(--border);\n      border-radius:6px;\n    }\n    button:hover{background:#176a94;}\n    button:disabled{opacity:0.55; cursor:not-allowed;}\n\n    #dpiSelect, #alignmentLayoutSelect{\n      padding:6px 8px;\n      font-size:14px;\n      background:var(--bg-light);\n      color:var(--text);\n      border:1px solid var(--border);\n      border-radius:6px;\n    }\n\n    input[type=\"file\"]{color:var(--muted);}\n    input::file-selector-button{\n      background:var(--bg-light);\n      color:var(--text);\n      border:1px solid var(--border);\n      border-radius:6px;\n      padding:6px 10px;\n      cursor:pointer;\n    }\n\n    #dropZone{\n      border:2px dashed var(--border);\n      padding:30px 10px;\n      margin:10px auto 10px auto;\n      width:min(900px, 92vw);\n      background:var(--bg-light);\n      color:var(--text);\n      font-size:14px;\n      border-radius:10px;\n    }\n\n    #toolInfo{\n      width:min(900px, 92vw);\n      margin:10px auto 10px auto;\n      text-align:left;\n      padding:0 10px;\n    }\n\n    #sheetAreaProxy, #sheetAreaMPC{\n      max-width:650px;\n      margin:0 auto 20px auto;\n    }\n\n    .sheet{margin-bottom:20px;}\n    .gridFront,.gridBack{\n      display:grid;\n      grid-gap:5px;\n      max-width:600px;\n      margin:10px auto 25px auto;\n    }\n    .grid-title{font-weight:bold;margin-top:10px;}\n\n    .slot{\n      width:100%;\n      border:2px dashed var(--border);\n      display:flex;\n      align-items:center;\n      justify-content:center;\n      background:var(--bg-light);\n      position:relative;\n      overflow:hidden;\n      border-radius:8px;\n    }\n    .slot img{\n      width:100%;\n      height:100%;\n      object-fit:cover;\n      display:block;\n      user-select:none;\n      background:#ffffff00;\n    }\n\n    \/* Proxy 3x3 (MTG ratio) *\/\n    .slot.proxy{aspect-ratio:63\/88;}\n\n    \/* MPC 2x4 (sideways on paper). Box aspect = (bleedH \/ bleedW) *\/\n    .slot.mpc{aspect-ratio:3.6146\/2.6303;}\n\n    \/* MPC preview: rotated + fill slot visually (no distortion) *\/\n    .slot.mpc img{\n      object-fit:contain;\n      background:#ffffff;\n      transform-origin:center center;\n      transform:rotate(90deg) scale(1.3742);\n    }\n\n    .removeBtn{\n      position:absolute;\n      top:2px;\n      right:2px;\n      background:rgba(255,0,0,0.85);\n      color:#fff;\n      border:none;\n      font-size:14px;\n      padding:2px 6px;\n      border-radius:4px;\n      cursor:pointer;\n      display:none;\n    }\n    .doubleBtn{\n      position:absolute;\n      bottom:2px;\n      right:2px;\n      background:rgba(0,0,0,0.7);\n      color:#fff;\n      border:none;\n      font-size:11px;\n      padding:2px 6px;\n      border-radius:4px;\n      cursor:pointer;\n      display:none;\n    }\n    .slot:hover .removeBtn,\n    .slot:hover .doubleBtn{display:block;}\n\n    #alignmentControls{\n      width:min(900px, 92vw);\n      margin:10px auto 10px auto;\n      text-align:left;\n      font-size:14px;\n      border:1px solid var(--border);\n      padding:10px;\n      border-radius:10px;\n      background:var(--bg-light);\n    }\n    #alignmentControls h3{margin:0 0 8px 0;font-size:16px;}\n    .alignment-row{\n      margin-bottom:6px;\n      display:flex;\n      align-items:center;\n      gap:8px;\n      flex-wrap:wrap;\n    }\n    .alignment-row label{min-width:70px;}\n    .alignment-row input{\n      width:80px;\n      padding:4px 6px;\n      font-size:13px;\n      background:var(--bg);\n      color:var(--text);\n      border:1px solid var(--border);\n      border-radius:6px;\n    }\n    #alignmentControls small{display:block;margin-top:4px;color:var(--muted);}\n\n    #sheetControls{\n      width:min(900px, 92vw);\n      margin:10px auto 25px auto;\n      display:flex;\n      justify-content:center;\n      gap:10px;\n      flex-wrap:wrap;\n    }\n\n    \/* Modals *\/\n    #searchModal,#backModal,#doubleModal,#removeDoubleModal,#generatingModal{\n      position:fixed;inset:0;background:rgba(15,60,87,0.9);\n      display:none;align-items:center;justify-content:center;z-index:9999;\n    }\n    #searchModalContent,#backModalContent,#doubleModalContent,#removeDoubleModalContent,#generatingModalContent{\n      background:var(--bg);\n      padding:15px;border-radius:10px;\n      max-width:700px;width:95%;\n      max-height:80vh;display:flex;flex-direction:column;\n      border:1px solid var(--border);\n    }\n    #searchHeader,#backModalHeader,#doubleModalHeader,#removeDoubleModalHeader{\n      display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;\n    }\n    #searchHeader h2,#backModalHeader h2,#doubleModalHeader h2,#removeDoubleModalHeader h2{\n      margin:0;font-size:18px;\n    }\n    #searchClose,#backModalClose,#doubleModalClose,#removeDoubleModalClose{\n      cursor:pointer;border:none;background:transparent;font-size:18px;color:var(--text);\n    }\n\n    #searchInput{\n      width:100%;padding:8px;font-size:14px;margin-bottom:6px;\n      background:var(--bg-light);color:var(--text);\n      border:1px solid var(--border);border-radius:8px;\n    }\n    #suggestions{\n      display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;\n      max-height:80px;overflow-y:auto;border:1px solid var(--border);\n      padding:6px;font-size:13px;background:var(--bg-light);border-radius:8px;\n    }\n    .suggestionItem{\n      padding:4px 8px;border-radius:8px;border:1px solid var(--border);\n      cursor:pointer;background:var(--bg);white-space:nowrap;color:var(--text);\n    }\n    .suggestionItem:hover{background:var(--bg-light);}\n\n    #printResults{\n      flex:1;overflow-y:auto;border:1px solid var(--border);\n      padding:8px;display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));\n      gap:8px;background:var(--bg-light);border-radius:8px;\n    }\n    .printCard{\n      border:1px solid var(--border);border-radius:10px;padding:6px;cursor:pointer;\n      background:var(--bg);display:flex;flex-direction:column;align-items:center;\n      font-size:11px;color:var(--text);\n    }\n    .printCard img{width:100%;height:auto;margin-bottom:6px;border-radius:8px;}\n    .printMeta{text-align:center;}\n\n    #backOptions{\n      display:flex;flex-wrap:wrap;gap:10px;justify-content:center;\n    }\n    .backOption{\n      border:2px solid var(--border);border-radius:10px;padding:8px;width:170px;\n      cursor:pointer;background:var(--bg-light);\n      display:flex;flex-direction:column;align-items:center;font-size:12px;color:var(--text);\n    }\n    .backOption.selected{\n      border-color:var(--accent);\n      box-shadow:0 0 0 2px rgba(0,208,255,0.25);\n    }\n    .backOption img{width:100%;height:auto;margin-bottom:6px;border-radius:8px;}\n    .backLabel{text-align:center;margin-top:2px;}\n\n    #customBackInner,#doubleBackInner{\n      width:100%;height:110px;border:1px dashed var(--border);\n      display:flex;flex-direction:column;align-items:center;justify-content:center;\n      padding:6px;font-size:12px;background:var(--bg);color:var(--text);border-radius:10px;\n    }\n    #customBackInner.drag-over,#doubleBackInner.drag-over{\n      background:#176a94;border-color:var(--accent);\n    }\n\n    #generatingModal{z-index:10000;}\n    #generatingModalContent p{margin:0 0 6px 0;font-size:16px;text-align:center;}\n    #generatingModalContent small{color:var(--muted);font-size:13px;text-align:center;}\n\n    \/* Tab content visibility *\/\n    .tabPanel{display:none;}\n    .tabPanel.active{display:block;}\n  <\/style>\n<\/head>\n<body>\n  <div id=\"tabs\">\n    <button class=\"tabBtn active\" data-tab=\"proxy\">3\u00d73 No Bleed Edge (Scryfall + Upload)<\/button>\n    <button class=\"tabBtn\" data-tab=\"align\">Alignment Sheets<\/button>\n    <button class=\"tabBtn\" data-tab=\"mpc\">2\u00d74 Bleed Edge (Upload)<\/button>\n  <\/div>\n\n  <h1 style=\"margin:12px 10px 0 10px;\">TCG Print Tools<\/h1>\n\n  <div id=\"toolInfo\">\n    <div id=\"infoProxy\" class=\"tabPanel active\">\n      <ul>\n        <li>Select or drag card images (fronts) onto the bar below, or click a blank slot and search via Scryfall.<\/li>\n        <li>Certain images from Scryfall may not transfer properly and you may need to manually download &#038; upload them.<\/li>\n        <li>Choose a default card back or upload your own. Hover a front to remove or make it double-faced.<\/li>\n        <li>Watch our YouTube tutorial on how to use this tool here: <a href=\"https:\/\/youtu.be\/LQhOK61izZw\" target=\"_blank\" rel=\"noopener noreferrer\">TCG PDF Generator How To<\/a><\/li>\n      <\/ul>\n    <\/div>\n\n    <div id=\"infoAlign\" class=\"tabPanel\">\n      <ul>\n        <li>Generates a PDF alignment sheet with a border grid you can measure in pixels.<\/li>\n        <li>Use this to calculate the offset needed to align your fronts to your backs.<\/li>\n        <li>Watch our YouTube tutorial on how to use this tool here: <a href=\"https:\/\/youtu.be\/mlZRnRteQYY\" target=\"_blank\" rel=\"noopener noreferrer\">Printer Alignment Sheet Tool Video<\/a><\/li>\n      <\/ul>\n    <\/div>\n\n    <div id=\"infoMPC\" class=\"tabPanel\">\n      <ul>\n        <li>This mode allows bleed edge and uses 2 columns \u00d7 4 rows (cards rotated sideways), to fit on a letter sized sheet.<\/li>\n        <li>Cut marks indicate the <strong>trim<\/strong> area (63mm\u00d788mm).<\/li>\n        <li>Watch our YouTube tutorial on how to use this tool here: <a href=\"https:\/\/youtu.be\/LQhOK61izZw\" target=\"_blank\" rel=\"noopener noreferrer\">TCG PDF Generator How To<\/a><\/li>\n      <\/ul>\n    <\/div>\n  <\/div>\n\n  <!-- Shared controls -->\n  <div id=\"controls\">\n    <input type=\"file\" id=\"fileInput\" multiple accept=\"image\/*\">\n    <button id=\"chooseBackBtn\">Choose card back<\/button>\n    <button id=\"clearBtn\">Clear<\/button>\n\n    <label for=\"dpiSelect\">PDF DPI:<\/label>\n    <select id=\"dpiSelect\">\n      <option value=\"300\" selected>300<\/option>\n      <option value=\"600\">600<\/option>\n      <option value=\"1200\">1200<\/option>\n    <\/select>\n  <\/div>\n\n  <div id=\"dropZone\">Drag &#038; Drop image files here to fill the active tool (auto-adds sheets).<\/div>\n\n  <!-- Alignment \/ offsets (shared) -->\n  <div id=\"alignmentControls\">\n    <h3>Print alignment offsets (pixels @ selected DPI)<\/h3>\n    <div class=\"alignment-row\">\n      <label>Front:<\/label>\n      <span>X:<\/span><input type=\"number\" id=\"frontOffsetX\" value=\"0\">\n      <span>Y:<\/span><input type=\"number\" id=\"frontOffsetY\" value=\"0\">\n    <\/div>\n    <div class=\"alignment-row\">\n      <label>Back:<\/label>\n      <span>X:<\/span><input type=\"number\" id=\"backOffsetX\" value=\"0\">\n      <span>Y:<\/span><input type=\"number\" id=\"backOffsetY\" value=\"0\">\n    <\/div>\n\n    <div id=\"alignmentExtra\" class=\"alignment-row tabPanel\" style=\"margin-top:8px;\">\n      <label>Layout:<\/label>\n      <select id=\"alignmentLayoutSelect\">\n        <option value=\"proxy\" selected>3\u00d73 Proxy<\/option>\n        <option value=\"mpc\">2\u00d74 MPCFill<\/option>\n      <\/select>\n    <\/div>\n\n    <small>Positive X moves right, negative left. Positive Y moves down, negative up.<\/small>\n  <\/div>\n\n  <div id=\"sheetControls\">\n    <button id=\"addSheetBtn\">Add sheet<\/button>\n    <button id=\"savePdf\">Save as PDF<\/button>\n  <\/div>\n\n  <!-- Sheet areas -->\n  <div id=\"sheetAreaProxy\" class=\"tabPanel active\"><\/div>\n  <div id=\"sheetAreaMPC\" class=\"tabPanel\"><\/div>\n\n  <canvas id=\"cardCanvas\" style=\"display:none;\"><\/canvas>\n\n  <!-- Scryfall search modal (proxy mode only) -->\n  <div id=\"searchModal\">\n    <div id=\"searchModalContent\">\n      <div id=\"searchHeader\">\n        <h2>Search Scryfall<\/h2>\n        <button id=\"searchClose\" title=\"Close\">\u2715<\/button>\n      <\/div>\n      <input id=\"searchInput\" type=\"text\" placeholder=\"Type a card name\u2026\" autocomplete=\"off\">\n      <div id=\"suggestions\"><\/div>\n      <div id=\"printResults\"><\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Global back selection modal -->\n  <div id=\"backModal\">\n    <div id=\"backModalContent\">\n      <div id=\"backModalHeader\">\n        <h2>Choose Card Back<\/h2>\n        <button id=\"backModalClose\" title=\"Close\">\u2715<\/button>\n      <\/div>\n      <div id=\"backOptions\">\n        <div class=\"backOption\" data-url=\"https:\/\/hiropaper.ca\/backs\/back1.png\" id=\"backOption1\">\n          <img decoding=\"async\" src=\"https:\/\/hiropaper.ca\/backs\/back1.png\" alt=\"Back 1\">\n          <div class=\"backLabel\">Hiro Back 1<\/div>\n        <\/div>\n        <div class=\"backOption\" data-url=\"https:\/\/hiropaper.ca\/backs\/back2.png\" id=\"backOption2\">\n          <img decoding=\"async\" src=\"https:\/\/hiropaper.ca\/backs\/back2.png\" alt=\"Back 2\">\n          <div class=\"backLabel\">Hiro Back 2<\/div>\n        <\/div>\n        <div class=\"backOption\" id=\"customBackOption\">\n          <div id=\"customBackInner\">\n            <strong>Custom Back<\/strong>\n            <span>Click or drop an image<\/span>\n          <\/div>\n          <div class=\"backLabel\">Your own image<\/div>\n          <input type=\"file\" id=\"customBackFile\" accept=\"image\/*\" style=\"display:none;\">\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Per-card double-faced back modal -->\n  <div id=\"doubleModal\">\n    <div id=\"doubleModalContent\">\n      <div id=\"doubleModalHeader\">\n        <h2>Choose Back For This Card<\/h2>\n        <button id=\"doubleModalClose\" title=\"Close\">\u2715<\/button>\n      <\/div>\n      <div class=\"backOption\" style=\"margin:0 auto;\">\n        <div id=\"doubleBackInner\">\n          <strong>Double-faced Back<\/strong>\n          <span>Click or drop an image<\/span>\n        <\/div>\n        <div class=\"backLabel\">Custom back linked to this front<\/div>\n        <input type=\"file\" id=\"doubleBackFile\" accept=\"image\/*\" style=\"display:none;\">\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Confirm remove double-face modal -->\n  <div id=\"removeDoubleModal\">\n    <div id=\"removeDoubleModalContent\">\n      <div id=\"removeDoubleModalHeader\">\n        <h2>Remove back face?<\/h2>\n        <button id=\"removeDoubleModalClose\" title=\"Close\">\u2715<\/button>\n      <\/div>\n      <p>Are you sure you want to remove the back face of this card?<\/p>\n      <div style=\"display:flex; gap:8px; justify-content:flex-end; margin-top:10px;\">\n        <button id=\"removeDoubleNo\">No<\/button>\n        <button id=\"removeDoubleYes\">Yes, remove back<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Generating PDF modal -->\n  <div id=\"generatingModal\">\n    <div id=\"generatingModalContent\">\n      <p>Generating PDF\u2026<\/p>\n      <small>Do not close this window until the download has started.<\/small>\n    <\/div>\n  <\/div>\n\n  <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jspdf\/2.5.1\/jspdf.umd.min.js\"><\/script>\n  <script>\n    \/* -------------------- DOM -------------------- *\/\n    const tabBtns = [...document.querySelectorAll(\".tabBtn\")];\n    const panelsInfo = {\n      proxy: document.getElementById(\"infoProxy\"),\n      align: document.getElementById(\"infoAlign\"),\n      mpc: document.getElementById(\"infoMPC\"),\n    };\n\n    const sheetAreaProxy = document.getElementById(\"sheetAreaProxy\");\n    const sheetAreaMPC = document.getElementById(\"sheetAreaMPC\");\n    const alignmentExtra = document.getElementById(\"alignmentExtra\");\n    const alignmentLayoutSelect = document.getElementById(\"alignmentLayoutSelect\");\n\n    const fileInput   = document.getElementById(\"fileInput\");\n    const canvas      = document.getElementById(\"cardCanvas\");\n    const ctx         = canvas.getContext(\"2d\");\n    const dropZone    = document.getElementById(\"dropZone\");\n    const clearBtn    = document.getElementById(\"clearBtn\");\n    const dpiSelect   = document.getElementById(\"dpiSelect\");\n    const addSheetBtn = document.getElementById(\"addSheetBtn\");\n    const savePdfBtn  = document.getElementById(\"savePdf\");\n\n    const frontOffsetXInput = document.getElementById(\"frontOffsetX\");\n    const frontOffsetYInput = document.getElementById(\"frontOffsetY\");\n    const backOffsetXInput  = document.getElementById(\"backOffsetX\");\n    const backOffsetYInput  = document.getElementById(\"backOffsetY\");\n\n    const chooseBackBtn     = document.getElementById(\"chooseBackBtn\");\n    const backModal         = document.getElementById(\"backModal\");\n    const backModalClose    = document.getElementById(\"backModalClose\");\n    const backOptions       = document.querySelectorAll(\".backOption[data-url]\");\n    const customBackOption  = document.getElementById(\"customBackOption\");\n    const customBackInner   = document.getElementById(\"customBackInner\");\n    const customBackFile    = document.getElementById(\"customBackFile\");\n\n    \/\/ Double-faced card modal elements\n    const doubleModal       = document.getElementById(\"doubleModal\");\n    const doubleModalClose  = document.getElementById(\"doubleModalClose\");\n    const doubleBackInner   = document.getElementById(\"doubleBackInner\");\n    const doubleBackFile    = document.getElementById(\"doubleBackFile\");\n    let currentDoubleFrontSlot = null;\n    let currentDoubleEngine = null;\n\n    \/\/ Remove-double-face modal elements\n    const removeDoubleModal       = document.getElementById(\"removeDoubleModal\");\n    const removeDoubleModalClose  = document.getElementById(\"removeDoubleModalClose\");\n    const removeDoubleNo          = document.getElementById(\"removeDoubleNo\");\n    const removeDoubleYes         = document.getElementById(\"removeDoubleYes\");\n    let currentRemoveFrontSlot    = null;\n    let currentRemoveEngine       = null;\n\n    \/\/ Generating modal\n    const generatingModal = document.getElementById(\"generatingModal\");\n    function showGeneratingModal(){ generatingModal.style.display = \"flex\"; }\n    function hideGeneratingModal(){ generatingModal.style.display = \"none\"; }\n\n    \/\/ Scryfall modal elements\n    const searchModal      = document.getElementById(\"searchModal\");\n    const searchInput      = document.getElementById(\"searchInput\");\n    const suggestionsDiv   = document.getElementById(\"suggestions\");\n    const printResultsDiv  = document.getElementById(\"printResults\");\n    const searchCloseBtn   = document.getElementById(\"searchClose\");\n    let currentSearchSlot = null;\n    let autocompleteTimeout = null;\n\n    \/* -------------------- Physical sizes -------------------- *\/\n    const PAGE_WIDTH_IN  = 8.5;\n    const PAGE_HEIGHT_IN = 11;\n\n    \/\/ Proxy: EXACT MTG trim size (63mm x 88mm)\n    const PROXY_CARD_W_IN = 63 \/ 25.4;\n    const PROXY_CARD_H_IN = 88 \/ 25.4;\n\n    \/\/ MPC 2\u00d74: EXACT MTG trim (63x88mm) with HARD-CODED letter-safe bleed.\n    \/\/ We intentionally reduce MPCFill's 1\/8\" bleed by trimming 0.05\" per side to fit letter printing.\n    \/\/ Effective bleed per side = 0.125 - 0.05 = 0.075\"\n    const MPC_TRIM_W_IN = 63 \/ 25.4;\n    const MPC_TRIM_H_IN = 88 \/ 25.4;\n    const MPC_BLEED_PER_SIDE_IN = 0.075;\n\n    \/\/ Bleed art (upright) dimensions\n    const MPC_ART_W_IN = MPC_TRIM_W_IN + (2 * MPC_BLEED_PER_SIDE_IN);\n    const MPC_ART_H_IN = MPC_TRIM_H_IN + (2 * MPC_BLEED_PER_SIDE_IN);\n\n    \/\/ Placed sideways on portrait page (box width = upright height, box height = upright width)\n    const MPC_BOX_W_IN = MPC_ART_H_IN;\n    const MPC_BOX_H_IN = MPC_ART_W_IN;\n\n    const MPC_GAP_IN = 0.0; \/\/ no intentional gap by default\n\n    \/* -------------------- Back sets -------------------- *\/\n    const PROXY_BACKS = [\n      \"https:\/\/hiropaper.ca\/backs\/back1.png\",\n      \"https:\/\/hiropaper.ca\/backs\/back2.png\",\n    ];\n\n    const MPC_BACKS = [\n      \"https:\/\/hiropaper.ca\/backs\/back1bleed.png\",\n      \"https:\/\/hiropaper.ca\/backs\/back2bleed.png\",\n    ];\n\n    \/\/ Remember the user's choice per tab\n    let selectedProxyBack = PROXY_BACKS[0];\n    let selectedMpcBack   = MPC_BACKS[0];\n\n    \/\/ Current active default\n    let defaultBackSrc = selectedProxyBack;\n\n    \/* -------------------- Mode state -------------------- *\/\n    let activeTab = \"proxy\"; \/\/ \"proxy\" | \"align\" | \"mpc\"\n\n    function dpi(){ return parseInt(dpiSelect.value, 10) || 300; }\n\n    function getOffsets(isBack){\n      const fx = Number(frontOffsetXInput.value) || 0;\n      const fy = Number(frontOffsetYInput.value) || 0;\n      const bx = Number(backOffsetXInput.value)  || 0;\n      const by = Number(backOffsetYInput.value)  || 0;\n      return isBack ? {x: bx, y: by} : {x: fx, y: fy};\n    }\n\n    function setSelectedBackOption(el){\n      document.querySelectorAll(\".backOption\").forEach(opt => opt.classList.remove(\"selected\"));\n      if (el) el.classList.add(\"selected\");\n    }\n\n    function getCutLineWidthPx(d){\n      \/\/ thicker cut marks for print visibility: 300\u21922, 600\u21924, 1200\u21928\n      return Math.max(2, Math.round((d \/ 300) * 2));\n    }\n\n    function syncBackOptionsForActiveTab(){\n      const isMpc = (activeTab === \"mpc\");\n      const backs = isMpc ? MPC_BACKS : PROXY_BACKS;\n\n      const opt1 = document.getElementById(\"backOption1\");\n      const opt2 = document.getElementById(\"backOption2\");\n\n      opt1.dataset.url = backs[0];\n      opt1.querySelector(\"img\").src = backs[0];\n      opt1.querySelector(\".backLabel\").textContent = isMpc ? \"Back 1 (Bleed Edge)\" : \"Back 1\";\n\n      opt2.dataset.url = backs[1];\n      opt2.querySelector(\"img\").src = backs[1];\n      opt2.querySelector(\".backLabel\").textContent = isMpc ? \"Back 2 (Bleed Edge)\" : \"Back 2\";\n\n      const selected = isMpc ? selectedMpcBack : selectedProxyBack;\n      if (selected === backs[0]) setSelectedBackOption(opt1);\n      else if (selected === backs[1]) setSelectedBackOption(opt2);\n      \/\/ else custom stays selected\n    }\n\n    function setTab(tab){\n      activeTab = tab;\n\n      tabBtns.forEach(b => b.classList.toggle(\"active\", b.dataset.tab === tab));\n\n      Object.keys(panelsInfo).forEach(k => {\n        panelsInfo[k].classList.toggle(\"active\", k === tab);\n      });\n\n      sheetAreaProxy.classList.toggle(\"active\", tab === \"proxy\");\n      sheetAreaMPC.classList.toggle(\"active\", tab === \"mpc\");\n\n      alignmentExtra.classList.toggle(\"active\", tab === \"align\");\n\n      chooseBackBtn.disabled = (tab === \"align\");\n      fileInput.disabled = (tab === \"align\");\n\n      if (tab === \"align\"){\n        addSheetBtn.disabled = true;\n        clearBtn.disabled = true;\n        savePdfBtn.textContent = \"Save Alignment PDF\";\n        dropZone.style.opacity = \"0.6\";\n        dropZone.textContent = \"Alignment mode: no file drop needed.\";\n      } else {\n        addSheetBtn.disabled = false;\n        clearBtn.disabled = false;\n        savePdfBtn.textContent = \"Save as PDF\";\n        dropZone.style.opacity = \"1\";\n        dropZone.textContent = tab === \"proxy\"\n          ? \"Drag & Drop image files here to fill fronts (auto-adds sheets).\"\n          : \"Drag & Drop image files here to fill fronts (auto-adds sheets).\";\n      }\n\n      \/\/ Switch default backs per tab\n      if (tab === \"mpc\") {\n        defaultBackSrc = selectedMpcBack;\n        mpcEngine.applyDefaultBacksAllSheets();\n      } else if (tab === \"proxy\") {\n        defaultBackSrc = selectedProxyBack;\n        proxyEngine.applyDefaultBacksAllSheets();\n      }\n\n      syncBackOptionsForActiveTab();\n    }\n\n    tabBtns.forEach(btn => btn.addEventListener(\"click\", () => setTab(btn.dataset.tab)));\n\n    \/* -------------------- Drag move (existing cards) -------------------- *\/\n    let draggedImg = null;\n\n    function handleDragStart(e){\n      draggedImg = e.target;\n      try{\n        e.dataTransfer.setData(\"text\/plain\", \"move\");\n        e.dataTransfer.effectAllowed = \"move\";\n      }catch{}\n    }\n    function handleDragEnd(){ draggedImg = null; }\n\n    \/* -------------------- Engines -------------------- *\/\n    function makeEngine(cfg){\n      const engine = { cfg, sheets: [] };\n\n      function createSlots(gridElement, isFront, sheetIndex){\n        const total = cfg.cols * cfg.rows;\n\n        for (let i=0;i<total;i++){\n          const slot = document.createElement(\"div\");\n          slot.classList.add(\"slot\", cfg.slotClass);\n          slot.dataset.index = i;\n          slot.dataset.sheetIndex = String(sheetIndex);\n          slot.dataset.side = isFront ? \"front\" : \"back\";\n          slot.dataset.mode = cfg.modeName;\n\n          if (isFront){\n            slot.ondragover = (e)=>e.preventDefault();\n            slot.ondrop = (e)=>handleDrop(e, engine);\n            slot.addEventListener(\"click\", ()=>handleSlotClick(slot, engine));\n          } else {\n            slot.addEventListener(\"click\", ()=>{});\n          }\n\n          gridElement.appendChild(slot);\n        }\n      }\n\n      function applyDefaultBacksToGrid(backGrid){\n        for (const slot of backGrid.children){\n          if (slot.dataset.doubleBack === \"true\") continue;\n          const img = new Image();\n          img.src = defaultBackSrc;\n          placeImageInSlot(slot, img);\n        }\n      }\n\n      function applyDefaultBacksAllSheets(){\n        engine.sheets.forEach(s => applyDefaultBacksToGrid(s.backGrid));\n      }\n\n      function createSheet(){\n        const sheetIndex = engine.sheets.length;\n\n        const sheetDiv = document.createElement(\"div\");\n        sheetDiv.className = \"sheet\";\n\n        const frontTitle = document.createElement(\"div\");\n        frontTitle.className = \"grid-title\";\n        frontTitle.textContent = `Sheet ${sheetIndex + 1} \u2013 Fronts`;\n\n        const frontGrid = document.createElement(\"div\");\n        frontGrid.className = \"gridFront\";\n        frontGrid.style.gridTemplateColumns = cfg.gridTemplateCols;\n\n        const backTitle = document.createElement(\"div\");\n        backTitle.className = \"grid-title\";\n        backTitle.textContent = `Sheet ${sheetIndex + 1} \u2013 Backs`;\n\n        const backGrid = document.createElement(\"div\");\n        backGrid.className = \"gridBack\";\n        backGrid.style.gridTemplateColumns = cfg.gridTemplateCols;\n\n        sheetDiv.appendChild(frontTitle);\n        sheetDiv.appendChild(frontGrid);\n        sheetDiv.appendChild(backTitle);\n        sheetDiv.appendChild(backGrid);\n\n        cfg.sheetAreaEl.appendChild(sheetDiv);\n\n        createSlots(frontGrid, true, sheetIndex);\n        createSlots(backGrid, false, sheetIndex);\n        applyDefaultBacksToGrid(backGrid);\n\n        const sheet = { frontGrid, backGrid, sheetDiv };\n        engine.sheets.push(sheet);\n        return sheet;\n      }\n\n      function getBackSlotForFrontSlot(frontSlot){\n        const sheetIndex = parseInt(frontSlot.dataset.sheetIndex, 10);\n        const sheet = engine.sheets[sheetIndex];\n        const frontIndex = [...sheet.frontGrid.children].indexOf(frontSlot);\n        if (frontIndex === -1) return null;\n        const backIndex = cfg.backIndexFromFrontIndex(frontIndex);\n        return sheet.backGrid.children[backIndex];\n      }\n\n      function isFrontDoubleFaced(frontSlot){\n        const backSlot = getBackSlotForFrontSlot(frontSlot);\n        return !!(backSlot && backSlot.dataset.doubleBack === \"true\");\n      }\n\n      function updateDoubleButtonLabel(frontSlot, btn){\n        if (!btn) return;\n        btn.textContent = isFrontDoubleFaced(frontSlot) ? \"Remove double face\" : \"Make double faced\";\n      }\n\n      function makeRemoveButton(slot){\n        const btn = document.createElement(\"button\");\n        btn.textContent = \"\u2715\";\n        btn.className = \"removeBtn\";\n        btn.onclick = (e)=>{\n          e.stopPropagation();\n\n          const backSlot = getBackSlotForFrontSlot(slot);\n          if (backSlot){\n            delete backSlot.dataset.doubleBack;\n            const img = new Image();\n            img.src = defaultBackSrc;\n            placeImageInSlot(backSlot, img);\n          }\n          slot.innerHTML = \"\";\n        };\n        return btn;\n      }\n\n      function handleDoubleButtonClick(slot){\n        if (isFrontDoubleFaced(slot)){\n          openRemoveDoubleModal(slot, engine);\n        } else {\n          openDoubleModalForFront(slot, engine);\n        }\n      }\n\n      function makeDoubleButton(slot){\n        const btn = document.createElement(\"button\");\n        btn.className = \"doubleBtn\";\n        updateDoubleButtonLabel(slot, btn);\n        btn.onclick = (e)=>{\n          e.stopPropagation();\n          handleDoubleButtonClick(slot);\n        };\n        return btn;\n      }\n\n      function placeImageInSlot(slot, img){\n        const isFront = (slot.dataset.side === \"front\");\n\n        slot.innerHTML = \"\";\n\n        if (isFront){\n          img.draggable = true;\n          img.ondragstart = handleDragStart;\n          img.ondragend = handleDragEnd;\n        } else {\n          img.draggable = false;\n          img.ondragstart = null;\n          img.ondragend = null;\n        }\n\n        slot.appendChild(img);\n\n        if (isFront){\n          slot.appendChild(makeRemoveButton(slot));\n          slot.appendChild(makeDoubleButton(slot));\n        }\n      }\n\n      function getEmptyFrontSlots(){\n        const slots = [];\n        engine.sheets.forEach(sheet => {\n          [...sheet.frontGrid.children].forEach(slot => {\n            if (!slot.querySelector(\"img\")) slots.push(slot);\n          });\n        });\n        return slots;\n      }\n\n      function clearAll(){\n        engine.sheets.forEach(sheet => {\n          for (const slot of sheet.frontGrid.children) slot.innerHTML = \"\";\n          for (const slot of sheet.backGrid.children){\n            slot.innerHTML = \"\";\n            delete slot.dataset.doubleBack;\n          }\n        });\n        applyDefaultBacksAllSheets();\n      }\n\n      \/\/ init first sheet\n      createSheet();\n\n      engine.createSheet = createSheet;\n      engine.applyDefaultBacksAllSheets = applyDefaultBacksAllSheets;\n      engine.getBackSlotForFrontSlot = getBackSlotForFrontSlot;\n      engine.isFrontDoubleFaced = isFrontDoubleFaced;\n      engine.updateDoubleButtonLabel = updateDoubleButtonLabel;\n      engine.placeImageInSlot = placeImageInSlot;\n      engine.getEmptyFrontSlots = getEmptyFrontSlots;\n      engine.clearAll = clearAll;\n\n      return engine;\n    }\n\n    const proxyEngine = makeEngine({\n      modeName: \"proxy\",\n      sheetAreaEl: sheetAreaProxy,\n      cols: 3, rows: 3,\n      slotClass: \"proxy\",\n      gridTemplateCols: \"repeat(3, 1fr)\",\n      cardWIn: PROXY_CARD_W_IN,\n      cardHIn: PROXY_CARD_H_IN,\n      gapIn: 0,\n      backIndexFromFrontIndex: (frontIndex) => {\n        const row = Math.floor(frontIndex \/ 3);\n        const col = frontIndex % 3;\n        const mirroredCol = 2 - col;\n        return row * 3 + mirroredCol;\n      },\n      rotateDeg: 0,\n      pdfDrawMode: \"stretch\"\n    });\n\n    const mpcEngine = makeEngine({\n      modeName: \"mpc\",\n      sheetAreaEl: sheetAreaMPC,\n      cols: 2, rows: 4,\n      slotClass: \"mpc\",\n      gridTemplateCols: \"repeat(2, 1fr)\",\n      cardWIn: MPC_BOX_W_IN,\n      cardHIn: MPC_BOX_H_IN,\n      gapIn: MPC_GAP_IN,\n      backIndexFromFrontIndex: (frontIndex) => {\n        const row = Math.floor(frontIndex \/ 2);\n        const col = frontIndex % 2;\n        const mirroredCol = 1 - col;\n        return row * 2 + mirroredCol;\n      },\n      rotateDeg: 90,\n      pdfDrawMode: \"containClip\"\n    });\n\n    function getActiveEngine(){\n      if (activeTab === \"proxy\") return proxyEngine;\n      if (activeTab === \"mpc\") return mpcEngine;\n      return null;\n    }\n\n    \/* -------------------- Move cards (drop) -------------------- *\/\n    function handleDrop(e, engine){\n      e.preventDefault();\n      if (!draggedImg) return;\n\n      const targetSlot = e.currentTarget;\n      const originSlot = draggedImg.parentElement;\n\n      if (!originSlot || originSlot === targetSlot){\n        draggedImg = null;\n        return;\n      }\n\n      \/\/ Only allow moving within the same engine\/mode\n      if (originSlot.dataset.mode !== targetSlot.dataset.mode){\n        draggedImg = null;\n        return;\n      }\n\n      const cfg = engine.cfg;\n\n      const originSheetIndex = parseInt(originSlot.dataset.sheetIndex, 10);\n      const targetSheetIndex = parseInt(targetSlot.dataset.sheetIndex, 10);\n      const originSheet = engine.sheets[originSheetIndex];\n      const targetSheet = engine.sheets[targetSheetIndex];\n\n      const originImg = originSlot.querySelector(\"img\");\n      const targetImg = targetSlot.querySelector(\"img\");\n\n      if (!originImg){\n        draggedImg = null;\n        return;\n      }\n\n      if (targetImg){\n        engine.placeImageInSlot(originSlot, targetImg);\n        engine.placeImageInSlot(targetSlot, originImg);\n      } else {\n        originSlot.innerHTML = \"\";\n        engine.placeImageInSlot(targetSlot, originImg);\n      }\n\n      \/\/ Move corresponding backs with the same swap logic\n      const originFrontIndex = [...originSheet.frontGrid.children].indexOf(originSlot);\n      const targetFrontIndex = [...targetSheet.frontGrid.children].indexOf(targetSlot);\n\n      const backOriginIndex = cfg.backIndexFromFrontIndex(originFrontIndex);\n      const backTargetIndex = cfg.backIndexFromFrontIndex(targetFrontIndex);\n\n      const backOriginSlot = originSheet.backGrid.children[backOriginIndex];\n      const backTargetSlot = targetSheet.backGrid.children[backTargetIndex];\n\n      const backOriginImg = backOriginSlot.querySelector(\"img\");\n      const backTargetImg = backTargetSlot.querySelector(\"img\");\n\n      const originDouble = backOriginSlot.dataset.doubleBack === \"true\";\n      const targetDouble = backTargetSlot.dataset.doubleBack === \"true\";\n\n      if (backOriginImg || backTargetImg){\n        if (backOriginImg && backTargetImg){\n          engine.placeImageInSlot(backOriginSlot, backTargetImg);\n          engine.placeImageInSlot(backTargetSlot, backOriginImg);\n\n          if (targetDouble) backOriginSlot.dataset.doubleBack = \"true\"; else delete backOriginSlot.dataset.doubleBack;\n          if (originDouble) backTargetSlot.dataset.doubleBack = \"true\"; else delete backTargetSlot.dataset.doubleBack;\n        } else if (backOriginImg && !backTargetImg){\n          backOriginSlot.innerHTML = \"\";\n          delete backOriginSlot.dataset.doubleBack;\n          engine.placeImageInSlot(backTargetSlot, backOriginImg);\n          if (originDouble) backTargetSlot.dataset.doubleBack = \"true\"; else delete backTargetSlot.dataset.doubleBack;\n        } else if (!backOriginImg && backTargetImg){\n          backTargetSlot.innerHTML = \"\";\n          delete backTargetSlot.dataset.doubleBack;\n          engine.placeImageInSlot(backOriginSlot, backTargetImg);\n          if (targetDouble) backOriginSlot.dataset.doubleBack = \"true\"; else delete backOriginSlot.dataset.doubleBack;\n        }\n      }\n\n      \/\/ Refresh button labels\n      const originBtn = originSlot.querySelector(\".doubleBtn\");\n      if (originBtn) engine.updateDoubleButtonLabel(originSlot, originBtn);\n      const targetBtn = targetSlot.querySelector(\".doubleBtn\");\n      if (targetBtn) engine.updateDoubleButtonLabel(targetSlot, targetBtn);\n\n      draggedImg = null;\n    }\n\n    \/* -------------------- Front-slot click behavior -------------------- *\/\n    function handleSlotClick(slot, engine){\n      if (slot.dataset.side !== \"front\") return;\n      if (slot.querySelector(\"img\")) return;\n\n      if (engine.cfg.modeName === \"proxy\"){\n        openSearchModal(slot);\n      }\n    }\n\n    \/* -------------------- Upload\/drop files into active engine -------------------- *\/\n    fileInput.addEventListener(\"change\", (e)=>{\n      const eng = getActiveEngine();\n      if (!eng) return;\n      handleFiles([...e.target.files], eng);\n      fileInput.value = \"\";\n    });\n\n    dropZone.addEventListener(\"dragover\", (e)=>{\n      if (activeTab === \"align\") return;\n      e.preventDefault();\n    });\n\n    dropZone.addEventListener(\"drop\", (e)=>{\n      if (activeTab === \"align\") return;\n      e.preventDefault();\n      const eng = getActiveEngine();\n      if (!eng) return;\n      handleFiles([...e.dataTransfer.files], eng);\n    });\n\n    function handleFiles(files, engine){\n      if (!files.length) return;\n\n      let emptySlots = engine.getEmptyFrontSlots();\n      let needed = files.length;\n\n      while (emptySlots.length < needed){\n        engine.createSheet();\n        emptySlots = engine.getEmptyFrontSlots();\n      }\n\n      const filesToPlace = files.slice(0, emptySlots.length);\n\n      filesToPlace.forEach((file, i)=>{\n        const slot = emptySlots[i];\n        const img = new Image();\n        img.src = URL.createObjectURL(file);\n        img.onload = ()=> engine.placeImageInSlot(slot, img);\n      });\n    }\n\n    \/* -------------------- Clear \/ Add sheet -------------------- *\/\n    clearBtn.addEventListener(\"click\", ()=>{\n      if (activeTab === \"align\") return;\n      const eng = getActiveEngine();\n      if (!eng) return;\n      eng.clearAll();\n    });\n\n    addSheetBtn.addEventListener(\"click\", ()=>{\n      const eng = getActiveEngine();\n      if (!eng) return;\n      const sheet = eng.createSheet();\n      sheet.sheetDiv?.scrollIntoView({behavior:\"smooth\", block:\"start\"});\n    });\n\n    \/* -------------------- Global back chooser -------------------- *\/\n    function openBackModal(){\n      syncBackOptionsForActiveTab();\n      backModal.style.display = \"flex\";\n    }\n    function closeBackModal(){ backModal.style.display = \"none\"; }\n\n    chooseBackBtn.addEventListener(\"click\", openBackModal);\n    backModalClose.addEventListener(\"click\", closeBackModal);\n    backModal.addEventListener(\"click\", (e)=>{ if (e.target === backModal) closeBackModal(); });\n\n    backOptions.forEach(opt=>{\n      opt.addEventListener(\"click\", ()=>{\n        const url = opt.dataset.url;\n        if (!url) return;\n\n        if (activeTab === \"mpc\"){\n          selectedMpcBack = url;\n          defaultBackSrc = selectedMpcBack;\n          mpcEngine.applyDefaultBacksAllSheets();\n        } else if (activeTab === \"proxy\"){\n          selectedProxyBack = url;\n          defaultBackSrc = selectedProxyBack;\n          proxyEngine.applyDefaultBacksAllSheets();\n        }\n\n        setSelectedBackOption(opt);\n        closeBackModal();\n      });\n    });\n\n    function handleCustomBackFile(file){\n      if (!file) return;\n      const url = URL.createObjectURL(file);\n\n      if (activeTab === \"mpc\"){\n        selectedMpcBack = url;\n        defaultBackSrc = selectedMpcBack;\n        mpcEngine.applyDefaultBacksAllSheets();\n      } else if (activeTab === \"proxy\"){\n        selectedProxyBack = url;\n        defaultBackSrc = selectedProxyBack;\n        proxyEngine.applyDefaultBacksAllSheets();\n      }\n\n      setSelectedBackOption(customBackOption);\n\n      customBackInner.innerHTML = \"\";\n      const img = document.createElement(\"img\");\n      img.src = url;\n      img.style.maxWidth = \"100%\";\n      img.style.maxHeight = \"100%\";\n      customBackInner.appendChild(img);\n\n      closeBackModal();\n    }\n\n    customBackOption.addEventListener(\"click\", ()=>customBackFile.click());\n    customBackFile.addEventListener(\"change\", (e)=>{\n      handleCustomBackFile(e.target.files[0]);\n      customBackFile.value = \"\";\n    });\n\n    customBackInner.addEventListener(\"dragover\", (e)=>{\n      e.preventDefault();\n      customBackInner.classList.add(\"drag-over\");\n    });\n    customBackInner.addEventListener(\"dragleave\", (e)=>{\n      e.preventDefault();\n      customBackInner.classList.remove(\"drag-over\");\n    });\n    customBackInner.addEventListener(\"drop\", (e)=>{\n      e.preventDefault();\n      customBackInner.classList.remove(\"drag-over\");\n      handleCustomBackFile(e.dataTransfer.files[0]);\n    });\n\n    \/* -------------------- Double-faced modal logic -------------------- *\/\n    function openDoubleModalForFront(slot, engine){\n      const img = slot.querySelector(\"img\");\n      if (!img) return;\n\n      currentDoubleFrontSlot = slot;\n      currentDoubleEngine = engine;\n\n      doubleBackInner.innerHTML = \"<strong>Double-faced Back<\/strong><span>Click or drop an image<\/span>\";\n      doubleModal.style.display = \"flex\";\n    }\n\n    function closeDoubleModal(){\n      doubleModal.style.display = \"none\";\n      currentDoubleFrontSlot = null;\n      currentDoubleEngine = null;\n      doubleBackFile.value = \"\";\n    }\n\n    doubleModalClose.addEventListener(\"click\", closeDoubleModal);\n    doubleModal.addEventListener(\"click\", (e)=>{ if (e.target === doubleModal) closeDoubleModal(); });\n\n    function handleDoubleBackFile(file){\n      if (!file || !currentDoubleFrontSlot || !currentDoubleEngine) return;\n\n      const url = URL.createObjectURL(file);\n      const img = new Image();\n      img.src = url;\n      img.onload = ()=>{\n        const backSlot = currentDoubleEngine.getBackSlotForFrontSlot(currentDoubleFrontSlot);\n        if (!backSlot) return;\n\n        currentDoubleEngine.placeImageInSlot(backSlot, img);\n        backSlot.dataset.doubleBack = \"true\";\n\n        const btn = currentDoubleFrontSlot.querySelector(\".doubleBtn\");\n        if (btn) currentDoubleEngine.updateDoubleButtonLabel(currentDoubleFrontSlot, btn);\n\n        doubleBackInner.innerHTML = \"\";\n        const preview = document.createElement(\"img\");\n        preview.src = url;\n        preview.style.maxWidth = \"100%\";\n        preview.style.maxHeight = \"100%\";\n        doubleBackInner.appendChild(preview);\n\n        setTimeout(closeDoubleModal, 250);\n      };\n    }\n\n    doubleBackInner.addEventListener(\"click\", ()=>doubleBackFile.click());\n    doubleBackFile.addEventListener(\"change\", (e)=>{\n      handleDoubleBackFile(e.target.files[0]);\n      doubleBackFile.value = \"\";\n    });\n\n    doubleBackInner.addEventListener(\"dragover\", (e)=>{\n      e.preventDefault();\n      doubleBackInner.classList.add(\"drag-over\");\n    });\n    doubleBackInner.addEventListener(\"dragleave\", (e)=>{\n      e.preventDefault();\n      doubleBackInner.classList.remove(\"drag-over\");\n    });\n    doubleBackInner.addEventListener(\"drop\", (e)=>{\n      e.preventDefault();\n      doubleBackInner.classList.remove(\"drag-over\");\n      handleDoubleBackFile(e.dataTransfer.files[0]);\n    });\n\n    \/* -------------------- Remove double-faced confirmation -------------------- *\/\n    function openRemoveDoubleModal(slot, engine){\n      currentRemoveFrontSlot = slot;\n      currentRemoveEngine = engine;\n      removeDoubleModal.style.display = \"flex\";\n    }\n\n    function closeRemoveDoubleModal(){\n      removeDoubleModal.style.display = \"none\";\n      currentRemoveFrontSlot = null;\n      currentRemoveEngine = null;\n    }\n\n    removeDoubleModalClose.addEventListener(\"click\", closeRemoveDoubleModal);\n    removeDoubleNo.addEventListener(\"click\", closeRemoveDoubleModal);\n    removeDoubleModal.addEventListener(\"click\", (e)=>{ if (e.target === removeDoubleModal) closeRemoveDoubleModal(); });\n\n    removeDoubleYes.addEventListener(\"click\", ()=>{\n      if (!currentRemoveFrontSlot || !currentRemoveEngine){\n        closeRemoveDoubleModal();\n        return;\n      }\n\n      const backSlot = currentRemoveEngine.getBackSlotForFrontSlot(currentRemoveFrontSlot);\n      if (backSlot){\n        delete backSlot.dataset.doubleBack;\n        const img = new Image();\n        img.src = defaultBackSrc;\n        currentRemoveEngine.placeImageInSlot(backSlot, img);\n      }\n\n      const btn = currentRemoveFrontSlot.querySelector(\".doubleBtn\");\n      if (btn) currentRemoveEngine.updateDoubleButtonLabel(currentRemoveFrontSlot, btn);\n\n      closeRemoveDoubleModal();\n    });\n\n    \/* -------------------- Scryfall (Proxy mode only) -------------------- *\/\n    function openSearchModal(slot){\n      currentSearchSlot = slot;\n      searchInput.value = \"\";\n      suggestionsDiv.innerHTML = \"\";\n      printResultsDiv.innerHTML = \"\";\n      searchModal.style.display = \"flex\";\n      searchInput.focus();\n    }\n\n    function closeSearchModal(){\n      searchModal.style.display = \"none\";\n      currentSearchSlot = null;\n    }\n\n    searchCloseBtn.addEventListener(\"click\", closeSearchModal);\n    searchModal.addEventListener(\"click\", (e)=>{ if (e.target === searchModal) closeSearchModal(); });\n\n    searchInput.addEventListener(\"input\", ()=>{\n      const term = searchInput.value.trim();\n      suggestionsDiv.innerHTML = \"\";\n      printResultsDiv.innerHTML = \"\";\n\n      if (term.length < 2) return;\n\n      clearTimeout(autocompleteTimeout);\n      autocompleteTimeout = setTimeout(()=>fetchAutocomplete(term), 200);\n    });\n\n    async function fetchAutocomplete(term){\n      try{\n        const url = \"https:\/\/api.scryfall.com\/cards\/autocomplete?q=\" + encodeURIComponent(term);\n        const res = await fetch(url);\n        if (!res.ok) return;\n\n        const data = await res.json();\n        const names = data.data || [];\n\n        suggestionsDiv.innerHTML = \"\";\n        names.forEach(name=>{\n          const btn = document.createElement(\"div\");\n          btn.className = \"suggestionItem\";\n          btn.textContent = name;\n          btn.onclick = ()=>{\n            searchInput.value = name;\n            suggestionsDiv.innerHTML = \"\";\n            fetchPrintsForName(name);\n          };\n          suggestionsDiv.appendChild(btn);\n        });\n      } catch(err){\n        console.error(err);\n      }\n    }\n\n    async function fetchPrintsForName(name){\n      try{\n        printResultsDiv.innerHTML = \"Loading printings\u2026\";\n        const safeName = String(name).split(\"\/\").join(\"\\\\\/\");\n        const q = `name:\/^${safeName}$\/ game:paper`;\n\n        const url = \"https:\/\/api.scryfall.com\/cards\/search?q=\" +\n          encodeURIComponent(q) + \"&unique=prints&order=released&dir=desc\";\n\n        const res = await fetch(url);\n        if (!res.ok){\n          printResultsDiv.innerHTML = \"No results found.\";\n          return;\n        }\n\n        const data = await res.json();\n        const cards = data.data || [];\n        renderPrintResults(cards);\n      } catch(err){\n        console.error(err);\n        printResultsDiv.innerHTML = \"Error fetching printings.\";\n      }\n    }\n\n    function renderPrintResults(cards){\n      printResultsDiv.innerHTML = \"\";\n      if (!cards.length){\n        printResultsDiv.textContent = \"No printings found.\";\n        return;\n      }\n\n      cards.forEach(card=>{\n        const cardDiv = document.createElement(\"div\");\n        cardDiv.className = \"printCard\";\n\n        let thumbUrl = null;\n        if (card.image_uris){\n          thumbUrl = card.image_uris.small || card.image_uris.normal || card.image_uris.large;\n        } else if (card.card_faces?.length && card.card_faces[0].image_uris){\n          thumbUrl = card.card_faces[0].image_uris.small ||\n                     card.card_faces[0].image_uris.normal ||\n                     card.card_faces[0].image_uris.large;\n        }\n\n        if (thumbUrl){\n          const img = document.createElement(\"img\");\n          img.src = thumbUrl;\n          img.alt = card.name;\n          cardDiv.appendChild(img);\n        }\n\n        const meta = document.createElement(\"div\");\n        meta.className = \"printMeta\";\n        meta.innerHTML = `\n          <div>${card.name}<\/div>\n          <div>${(card.set_name || \"\").substring(0, 22)}<\/div>\n          <div>${card.set ? card.set.toUpperCase() : \"\"} #${card.collector_number || \"\"}<\/div>\n        `;\n        cardDiv.appendChild(meta);\n\n        cardDiv.onclick = ()=>choosePrinting(card);\n        printResultsDiv.appendChild(cardDiv);\n      });\n    }\n\n    function getFrontBackUrls(card){\n      let frontUrl = null;\n      let backUrl = null;\n\n      if (card.image_uris){\n        frontUrl = card.image_uris.png || card.image_uris.large || card.image_uris.normal;\n      }\n      if (card.card_faces?.length){\n        if (!frontUrl && card.card_faces[0].image_uris){\n          frontUrl = card.card_faces[0].image_uris.png ||\n                     card.card_faces[0].image_uris.large ||\n                     card.card_faces[0].image_uris.normal;\n        }\n        if (card.card_faces.length > 1 && card.card_faces[1].image_uris){\n          backUrl = card.card_faces[1].image_uris.png ||\n                    card.card_faces[1].image_uris.large ||\n                    card.card_faces[1].image_uris.normal;\n        }\n      }\n      return {frontUrl, backUrl};\n    }\n\n    async function choosePrinting(card){\n      if (!currentSearchSlot) return;\n\n      const {frontUrl, backUrl} = getFrontBackUrls(card);\n      if (!frontUrl){\n        alert(\"No image found for that printing.\");\n        return;\n      }\n\n      try{\n        const frontImg = new Image();\n        frontImg.crossOrigin = \"anonymous\";\n        frontImg.src = frontUrl;\n        await new Promise((resolve, reject)=>{ frontImg.onload=resolve; frontImg.onerror=reject; });\n        proxyEngine.placeImageInSlot(currentSearchSlot, frontImg);\n\n        if (backUrl){\n          const backSlot = proxyEngine.getBackSlotForFrontSlot(currentSearchSlot);\n          if (backSlot){\n            const backImg = new Image();\n            backImg.crossOrigin = \"anonymous\";\n            backImg.src = backUrl;\n            await new Promise((resolve, reject)=>{ backImg.onload=resolve; backImg.onerror=reject; });\n            proxyEngine.placeImageInSlot(backSlot, backImg);\n            backSlot.dataset.doubleBack = \"true\";\n\n            const btn = currentSearchSlot.querySelector(\".doubleBtn\");\n            if (btn) proxyEngine.updateDoubleButtonLabel(currentSearchSlot, btn);\n          }\n        }\n\n        closeSearchModal();\n      } catch(err){\n        console.error(err);\n        alert(\"Error loading image for that printing. This may be a CORS issue; you can download and upload the image manually instead.\");\n      }\n    }\n\n    \/* -------------------- Alignment sheet rendering -------------------- *\/\n    function drawAlignmentToCanvas(layoutKey, isBack){\n      const d = dpi();\n\n      const isProxy = (layoutKey === \"proxy\");\n      const cols = isProxy ? 3 : 2;\n      const rows = isProxy ? 3 : 4;\n      const cardWIn = isProxy ? PROXY_CARD_W_IN : MPC_BOX_W_IN;\n      const cardHIn = isProxy ? PROXY_CARD_H_IN : MPC_BOX_H_IN;\n      const gapIn = isProxy ? 0 : MPC_GAP_IN;\n\n      const pageWidthPx  = PAGE_WIDTH_IN  * d;\n      const pageHeightPx = PAGE_HEIGHT_IN * d;\n      canvas.width  = Math.round(pageWidthPx);\n      canvas.height = Math.round(pageHeightPx);\n\n      ctx.setTransform(1,0,0,1,0,0);\n      ctx.fillStyle = \"#FFFFFF\";\n      ctx.fillRect(0,0,canvas.width,canvas.height);\n\n      const cardWpx = cardWIn * d;\n      const cardHpx = cardHIn * d;\n      const gapPx = gapIn * d;\n\n      const gridWpx = (cols * cardWpx) + ((cols - 1) * gapPx);\n      const gridHpx = (rows * cardHpx) + ((rows - 1) * gapPx);\n\n      const baseOffsetX = (canvas.width  - gridWpx) \/ 2;\n      const baseOffsetY = (canvas.height - gridHpx) \/ 2;\n\n      const off = getOffsets(isBack);\n      const left = baseOffsetX + off.x;\n      const top  = baseOffsetY + off.y;\n      const right = left + gridWpx;\n      const bottom = top + gridHpx;\n\n      const step = 5 * (d \/ 300);     \/\/ 300\u21925, 600\u219210, 1200\u219220\n      const band = step * 10;         \/\/ 10 squares deep\n      const gridLineW = 1;            \/\/ keep alignment grid thin\n\n      function drawGridBand(clipX, clipY, clipW, clipH){\n        ctx.save();\n        ctx.beginPath();\n        ctx.rect(clipX, clipY, clipW, clipH);\n        ctx.clip();\n\n        ctx.strokeStyle = \"#000000\";\n        ctx.lineWidth = gridLineW;\n        ctx.beginPath();\n\n        for (let x = Math.ceil(left); x <= right; x += step){\n          ctx.moveTo(x + 0.5, top);\n          ctx.lineTo(x + 0.5, bottom);\n        }\n        for (let y = Math.ceil(top); y <= bottom; y += step){\n          ctx.moveTo(left, y + 0.5);\n          ctx.lineTo(right, y + 0.5);\n        }\n\n        ctx.stroke();\n        ctx.restore();\n      }\n\n      \/\/ Border-only bands\n      drawGridBand(left, top, gridWpx, Math.min(band, gridHpx));                                \/\/ top\n      drawGridBand(left, Math.max(top, bottom - band), gridWpx, Math.min(band, gridHpx));       \/\/ bottom\n      drawGridBand(left, top, Math.min(band, gridWpx), gridHpx);                                \/\/ left\n      drawGridBand(Math.max(left, right - band), top, Math.min(band, gridWpx), gridHpx);        \/\/ right\n\n      \/\/ Center label\n      const centerX = left + gridWpx \/ 2;\n      const centerY = top + gridHpx \/ 2;\n\n      ctx.save();\n      ctx.fillStyle = \"#000000\";\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n\n      const titleSize = Math.round(26 * (d \/ 300));\n      const subSize   = Math.round(16 * (d \/ 300));\n\n      ctx.font = `bold ${titleSize}px sans-serif`;\n      ctx.fillText(isBack ? \"BACK\" : \"FRONT\", centerX, centerY - (subSize * 2));\n\n      ctx.font = `${subSize}px sans-serif`;\n      ctx.fillText(`DPI: ${d}`, centerX, centerY);\n      ctx.fillText(`Each square = ${Math.round(step)} px`, centerX, centerY + (subSize * 2));\n      ctx.restore();\n\n      return canvas.toDataURL(\"image\/jpeg\", 1.0);\n    }\n\n    \/* -------------------- PDF drawing (sheets) -------------------- *\/\n    function drawSheetToCanvas(engine, gridElement, isBack){\n      const d = dpi();\n      const cfg = engine.cfg;\n\n      const pageWidthPx  = PAGE_WIDTH_IN  * d;\n      const pageHeightPx = PAGE_HEIGHT_IN * d;\n      canvas.width  = Math.round(pageWidthPx);\n      canvas.height = Math.round(pageHeightPx);\n\n      ctx.setTransform(1,0,0,1,0,0);\n      ctx.fillStyle = \"#FFFFFF\";\n      ctx.fillRect(0,0,canvas.width,canvas.height);\n\n      const cardWpx = cfg.cardWIn * d;\n      const cardHpx = cfg.cardHIn * d;\n      const gapPx   = cfg.gapIn * d;\n\n      const gridWpx = (cfg.cols * cardWpx) + ((cfg.cols - 1) * gapPx);\n      const gridHpx = (cfg.rows * cardHpx) + ((cfg.rows - 1) * gapPx);\n\n      const baseOffsetX = (canvas.width  - gridWpx) \/ 2;\n      const baseOffsetY = (canvas.height - gridHpx) \/ 2;\n\n      const off = getOffsets(isBack);\n      const appliedOffsetX = baseOffsetX + off.x;\n      const appliedOffsetY = baseOffsetY + off.y;\n\n      const total = cfg.cols * cfg.rows;\n\n      for (let i=0;i<total;i++){\n        const slot = gridElement.children[i];\n        const img = slot.querySelector(\"img\");\n        if (!img) continue;\n\n        const row = Math.floor(i \/ cfg.cols);\n        const col = i % cfg.cols;\n\n        const destX = appliedOffsetX + col * (cardWpx + gapPx);\n        const destY = appliedOffsetY + row * (cardHpx + gapPx);\n\n        \/\/ Clip to card rectangle\n        ctx.save();\n        ctx.beginPath();\n        ctx.rect(destX, destY, cardWpx, cardHpx);\n        ctx.clip();\n\n        if (cfg.pdfDrawMode === \"stretch\"){\n          ctx.drawImage(img, destX, destY, cardWpx, cardHpx);\n        } else if (cfg.pdfDrawMode === \"containClip\"){\n          const iw = img.naturalWidth || img.width;\n          const ih = img.naturalHeight || img.height;\n\n          const cx = destX + cardWpx\/2;\n          const cy = destY + cardHpx\/2;\n\n          ctx.translate(cx, cy);\n          if (cfg.rotateDeg){\n            ctx.rotate((cfg.rotateDeg * Math.PI) \/ 180);\n          }\n\n          const targetW = cardWpx;\n          const targetH = cardHpx;\n\n          const absRot = Math.abs(cfg.rotateDeg) % 180;\n          const srcWForFit = (absRot === 90) ? ih : iw;\n          const srcHForFit = (absRot === 90) ? iw : ih;\n\n          const scale = Math.min(targetW \/ srcWForFit, targetH \/ srcHForFit);\n\n          const drawW = iw * scale;\n          const drawH = ih * scale;\n\n          ctx.drawImage(img, -drawW\/2, -drawH\/2, drawW, drawH);\n        }\n\n        ctx.restore();\n      }\n\n      \/\/ CUT MARKS\n      const markLength = 0.2 * d;\n      const halfMark   = markLength \/ 2;\n\n      ctx.strokeStyle = \"#000000\";\n      ctx.lineWidth = getCutLineWidthPx(d);\n      ctx.beginPath();\n\n      if (cfg.modeName === \"mpc\") {\n        \/\/ MPC trim lines: inset equals our effective bleed per side\n        const trimInsetPx = MPC_BLEED_PER_SIDE_IN * d;\n\n        let xLines = [];\n        let yLines = [];\n\n        for (let c = 0; c < cfg.cols; c++) {\n          const boxLeft  = appliedOffsetX + c * (cardWpx + gapPx);\n          const boxRight = boxLeft + cardWpx;\n          xLines.push(boxLeft + trimInsetPx, boxRight - trimInsetPx);\n        }\n        for (let r = 0; r < cfg.rows; r++) {\n          const boxTop    = appliedOffsetY + r * (cardHpx + gapPx);\n          const boxBottom = boxTop + cardHpx;\n          yLines.push(boxTop + trimInsetPx, boxBottom - trimInsetPx);\n        }\n\n        xLines = [...new Set(xLines.map(v => Math.round(v)))].sort((a,b)=>a-b);\n        yLines = [...new Set(yLines.map(v => Math.round(v)))].sort((a,b)=>a-b);\n\n        const leftCut   = xLines[0];\n        const rightCut  = xLines[xLines.length - 1];\n        const topCut    = yLines[0];\n        const bottomCut = yLines[yLines.length - 1];\n\n        \/\/ Corner L-shapes in margins only (do not intrude into art)\n        ctx.moveTo(leftCut, topCut - markLength);      ctx.lineTo(leftCut, topCut - 1);\n        ctx.moveTo(leftCut - markLength, topCut);      ctx.lineTo(leftCut - 1, topCut);\n\n        ctx.moveTo(rightCut, topCut - markLength);     ctx.lineTo(rightCut, topCut - 1);\n        ctx.moveTo(rightCut + 1, topCut);              ctx.lineTo(rightCut + markLength, topCut);\n\n        ctx.moveTo(leftCut, bottomCut + 1);            ctx.lineTo(leftCut, bottomCut + markLength);\n        ctx.moveTo(leftCut - markLength, bottomCut);   ctx.lineTo(leftCut - 1, bottomCut);\n\n        ctx.moveTo(rightCut, bottomCut + 1);           ctx.lineTo(rightCut, bottomCut + markLength);\n        ctx.moveTo(rightCut + 1, bottomCut);           ctx.lineTo(rightCut + markLength, bottomCut);\n\n        \/\/ Vertical trim ticks ONLY above and below (margins)\n        for (let i = 0; i < xLines.length; i++) {\n          const x = xLines[i];\n          ctx.moveTo(x, topCut - halfMark);            ctx.lineTo(x, topCut - markLength);\n          ctx.moveTo(x, bottomCut + halfMark);         ctx.lineTo(x, bottomCut + markLength);\n        }\n\n        \/\/ Horizontal trim ticks ONLY left and right (margins)\n        for (let i = 0; i < yLines.length; i++) {\n          const y = yLines[i];\n          ctx.moveTo(leftCut - markLength, y);         ctx.lineTo(leftCut - halfMark, y);\n          ctx.moveTo(rightCut + halfMark, y);          ctx.lineTo(rightCut + markLength, y);\n        }\n\n      } else {\n        \/\/ Proxy mode: marks at card edges for the full grid (margin ticks)\n        const left   = appliedOffsetX;\n        const top    = appliedOffsetY;\n        const right  = appliedOffsetX + gridWpx;\n        const bottom = appliedOffsetY + gridHpx;\n\n        \/\/ Corner L-shapes\n        ctx.moveTo(left, top - markLength);   ctx.lineTo(left, top);\n        ctx.moveTo(left, top);               ctx.lineTo(left + markLength, top);\n\n        ctx.moveTo(right, top - markLength); ctx.lineTo(right, top);\n        ctx.moveTo(right - markLength, top); ctx.lineTo(right, top);\n\n        ctx.moveTo(left, bottom);            ctx.lineTo(left, bottom + markLength);\n        ctx.moveTo(left, bottom);            ctx.lineTo(left + markLength, bottom);\n\n        ctx.moveTo(right, bottom);           ctx.lineTo(right, bottom + markLength);\n        ctx.moveTo(right - markLength, bottom); ctx.lineTo(right, bottom);\n\n        \/\/ Vertical ticks between columns\n        for (let c=1;c<cfg.cols;c++){\n          const x = left + c * cardWpx + (c-1) * gapPx;\n          ctx.moveTo(x, top - halfMark);     ctx.lineTo(x, top);\n          ctx.moveTo(x, bottom);            ctx.lineTo(x, bottom + halfMark);\n        }\n\n        \/\/ Horizontal ticks between rows\n        for (let r=1;r<cfg.rows;r++){\n          const y = top + r * cardHpx + (r-1) * gapPx;\n          ctx.moveTo(left - halfMark, y);    ctx.lineTo(left, y);\n          ctx.moveTo(right, y);             ctx.lineTo(right + halfMark, y);\n        }\n      }\n\n      ctx.stroke();\n      return canvas.toDataURL(\"image\/jpeg\", 1.0);\n    }\n\n    \/* -------------------- Save PDF -------------------- *\/\n    savePdfBtn.addEventListener(\"click\", ()=>{\n      showGeneratingModal();\n\n      setTimeout(()=>{\n        try{\n          const { jsPDF } = window.jspdf;\n          const pdf = new jsPDF({orientation:\"portrait\", unit:\"pt\", format:\"letter\"});\n\n          if (activeTab === \"align\"){\n            const layoutKey = alignmentLayoutSelect.value || \"proxy\";\n\n            const frontImg = drawAlignmentToCanvas(layoutKey, false);\n            pdf.addImage(frontImg, \"JPEG\", 0, 0, 612, 792);\n\n            const backImg = drawAlignmentToCanvas(layoutKey, true);\n            pdf.addPage();\n            pdf.addImage(backImg, \"JPEG\", 0, 0, 612, 792);\n\n            pdf.save(`alignment-${layoutKey}-${dpi()}dpi.pdf`);\n          } else {\n            const engine = getActiveEngine();\n            if (!engine) return;\n\n            engine.sheets.forEach((sheet, index)=>{\n              const frontImg = drawSheetToCanvas(engine, sheet.frontGrid, false);\n\n              if (index === 0){\n                pdf.addImage(frontImg, \"JPEG\", 0, 0, 612, 792);\n              } else {\n                pdf.addPage();\n                pdf.addImage(frontImg, \"JPEG\", 0, 0, 612, 792);\n              }\n\n              const backImg = drawSheetToCanvas(engine, sheet.backGrid, true);\n              pdf.addPage();\n              pdf.addImage(backImg, \"JPEG\", 0, 0, 612, 792);\n            });\n\n            const name = (activeTab === \"proxy\") ? \"tcg-no-bleed\" : \"tcg-bleed\";\n            pdf.save(`${name}-${dpi()}dpi.pdf`);\n          }\n        } catch(err){\n          console.error(err);\n          alert(\"There was an error generating the PDF.\");\n        } finally {\n          hideGeneratingModal();\n        }\n      }, 50);\n    });\n\n    \/* -------------------- Init -------------------- *\/\n    setSelectedBackOption(document.getElementById(\"backOption1\"));\n    setTab(\"proxy\");\n  <\/script>\n<\/body>\n<\/html>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Hiro Paper \u2013 TCG Print Tools 3\u00d73 No Bleed Edge (Scryfall + Upload) Alignment Sheets 2\u00d74 Bleed Edge (Upload) TCG&#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-277","page","type-page","status-publish","hentry"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/277","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=277"}],"version-history":[{"count":31,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/277\/revisions"}],"predecessor-version":[{"id":694,"href":"https:\/\/hiropaper.ca\/index.php?rest_route=\/wp\/v2\/pages\/277\/revisions\/694"}],"wp:attachment":[{"href":"https:\/\/hiropaper.ca\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=277"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}