`; } function printFontCSS() { try { return [...document.querySelectorAll('style')].map(s => s.textContent).join('\n').match(/@font-face\s*{[^}]*}/g).join('\n'); } catch (e) { return ''; } } const PRINT_CSS = ` @page{size:__PAGE__;margin:9mm} *{box-sizing:border-box} html,body{margin:0;background:#fff;color:#1d1915} body{font:10pt/1.3 "CF Sans","Alegreya Sans",system-ui,sans-serif;-webkit-print-color-adjust:exact;print-color-adjust:exact} .page{page-break-after:always;break-after:page;position:relative;padding-bottom:16px} .page:last-child{page-break-after:auto;break-after:auto} footer{position:absolute;bottom:0;right:0;font-size:8pt;color:#7a6e62} small{font-size:7.5pt;color:#6b6055} .box{border:1.4px solid #3b322a;border-radius:8px;padding:5px 7px;margin-bottom:6px;break-inside:avoid} .t{font:700 8pt/1.2 "CF Sans",system-ui,sans-serif;letter-spacing:.08em;text-transform:uppercase;color:#3b322a;margin-bottom:4px;border-bottom:1px solid #cfc4b6;padding-bottom:2px} .l{font-size:7pt;text-transform:uppercase;letter-spacing:.06em;color:#5a4f45} .c{text-align:center} .big{font:800 18pt/1 "CF Sans",system-ui,sans-serif} .head{display:flex;gap:10px;align-items:stretch;margin-bottom:8px;border-bottom:2.5px solid #3b322a;padding-bottom:6px} .pic{width:30mm;height:30mm;border:1.6px solid #3b322a;border-radius:50%;overflow:hidden;flex:none} .pic img{width:100%;height:100%;object-fit:cover} .who{flex:1;min-width:0} .name{font:400 22pt/1.1 "CF Display","IM Fell English",Georgia,serif;border-bottom:1px solid #3b322a;padding-bottom:2px} .line{font-size:7pt;text-transform:uppercase;letter-spacing:.06em;color:#5a4f45;margin-bottom:4px} .grid6{display:grid;grid-template-columns:repeat(3,1fr);gap:3px 10px} .fld .v{border-bottom:1px solid #8a7d70;min-height:15px;font-weight:700;font-size:9.5pt;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .body1{display:grid;grid-template-columns:26mm 58mm 1fr;gap:7px} .abilities{display:flex;flex-direction:column;gap:6px} .ab{border:1.6px solid #3b322a;border-radius:12px 12px 18px 18px;text-align:center;padding:4px 2px 0;position:relative;height:30mm} .ab .l{font-weight:700;font-size:6.5pt} .ab .mod{font:800 20pt/1.2 "CF Sans",system-ui,sans-serif} .ab .score{position:absolute;left:50%;bottom:-1px;transform:translate(-50%,40%);background:#fff;border:1.4px solid #3b322a;border-radius:999px;min-width:11mm;font-weight:800;font-size:10pt;padding:0 4px} .pair{display:flex;align-items:center;gap:8px} .pair .big{min-width:12mm;text-align:center;font-size:14pt} .list .row{display:flex;align-items:center;gap:5px;font-size:8.6pt;line-height:1.45;border-bottom:1px dotted #d8cfc4} .list .row .num{min-width:7mm;text-align:right} .pip{display:inline-block;width:8px;height:8px;border:1.2px solid #3b322a;border-radius:50%;vertical-align:middle;margin-right:2px} .pip.on{background:#3b322a} .pip.x{border-radius:2px} .pip.lg{width:16px;height:16px} .trio{display:grid;grid-template-columns:repeat(3,1fr);gap:6px} .stat{text-align:center;padding:6px 2px} .shield{border-radius:8px 8px 40% 40%/8px 8px 30% 30%} .hp .hprow{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #cfc4b6;padding-bottom:2px} .hp .hpbig{height:16mm;font:800 24pt/16mm "CF Sans",system-ui,sans-serif;text-align:center} .hp .sm .wl{border-bottom:1px solid #8a7d70;min-width:22mm;text-align:center} .duo{display:grid;grid-template-columns:1fr 1fr;gap:6px} .dsr{display:flex;justify-content:space-between;align-items:center;font-size:8pt;margin:3px 0} table{width:100%;border-collapse:collapse} th{font-size:7pt;text-transform:uppercase;letter-spacing:.05em;color:#5a4f45;text-align:left;border-bottom:1px solid #8a7d70;padding:2px 3px} td{border-bottom:1px solid #d8cfc4;padding:3px;font-size:8.6pt;vertical-align:top} .txt{font-size:8.6pt;margin:2px 0} .ln{border-bottom:1px solid #cfc4b6;height:6.5mm} .wide{margin-top:12px} .cols2{display:grid;grid-template-columns:1.15fr 1fr;gap:8px;align-items:start} .feat{border-bottom:1px dotted #cfc4b6;padding:3px 0;break-inside:avoid} .fn{font-weight:800;font-size:9.4pt} .fd{font-size:8.2pt;color:#2b241e} .fd p{margin:2px 0} .fd ul{margin:2px 0 2px 14px;padding:0} table.mini th,table.mini td{font-size:7.5pt;padding:1px 3px} .coins{display:grid;grid-template-columns:repeat(5,1fr);gap:4px;text-align:center} .coins .big{font-size:12pt;border:1.2px solid #3b322a;border-radius:6px;padding:4px 0} .slots{display:flex;flex-wrap:wrap;gap:6px 14px;margin:6px 0} .slot .l{font-weight:700} .hint{font-size:7.5pt;color:#6b6055;margin-top:3px} .sp{table-layout:fixed}.sp td{font-size:8.2pt;overflow:hidden;text-overflow:ellipsis} .muted{color:#7a6e62} @media screen{body{background:#ece6dd;padding:12px}.page{background:#fff;max-width:200mm;margin:0 auto 14px;padding:9mm 9mm 16mm;box-shadow:0 2px 12px rgba(0,0,0,.18)}footer{right:9mm;bottom:5mm}} `; /* ---------- sending it to the printer (or a PDF) ---------- */ function printHTML(html, title) { if (printHTML.busy) return true; printHTML.busy = true; setTimeout(() => { printHTML.busy = false; }, 1500); if (window.CampfireAndroid && typeof window.CampfireAndroid.printHtml === 'function') { window.CampfireAndroid.printHtml(title, html); return true; } // browsers and iPhones: print just the sheet from this page try { const old = document.getElementById('printframe'); if (old) old.remove(); const holder = document.createElement('div'); holder.id = 'printframe'; const doc = new DOMParser().parseFromString(html, 'text/html'); const st = document.createElement('style'); st.textContent = '@media print{html,body{background:#fff!important;color:#1d1915!important;height:auto!important;overflow:visible!important}body>*:not(#printframe){display:none!important}#printframe{display:block!important}}@media screen{#printframe{display:none}}' + doc.querySelector('style').textContent.replace(/(^|})\s*([^{}@]+)\{/g, (m, b, sel) => b + ' ' + sel.split(',').map(x => /^\s*(html|body)\b/.test(x) ? '#printframe' : '#printframe ' + x.trim()).join(',') + '{'); holder.append(st, ...doc.body.childNodes); document.body.append(holder); const done = () => setTimeout(() => holder.remove(), 1000); window.addEventListener('afterprint', done, { once: true }); setTimeout(() => window.print(), 60); return true; } catch (e) { return false; } } function openPrintSheet(c, ctx = {}) { openSheet('Character sheet', rec => { const st = rec.state; if (!st.o) st.o = { paper: (navigator.language || '').toLowerCase() === 'en-us' ? 'Letter' : 'A4', spells: true, fullText: false, blankHP: false, player: ctx.player || '' }; const o = st.o; const make = () => sheetPrintHTML(c, Object.assign({}, o, { portrait: ctx.portrait })); const fname = (c.name || 'character').replace(/[^\w\- ]+/g, '').trim().replace(/\s+/g, '-').toLowerCase() + '-sheet.html'; const tog = (k, label, sub) => h('label', { class: 'pick' }, h('input', { type: 'checkbox', checked: !!o[k], onchange: e => { o[k] = e.target.checked; } }), h('span', { class: 'grow' }, label, sub ? h('div', { class: 'small muted' }, sub) : null)); add(rec.foot, [ !window.CampfireAndroid ? h('button', { class: 'btn', onclick: () => saveText(fname, make(), 'text/html') }, 'Save as file') : null, h('button', { class: 'btn primary', onclick: () => { if (!printHTML(make(), (c.name || 'Character') + ' sheet')) toast('Printing isn’t available here. Use Save as file.'); } }, icon('print'), window.CampfireAndroid ? 'Print or save PDF' : 'Print or PDF'), ]); return h('div', null, h('p', { class: 'muted', style: { marginTop: 0 } }, 'A paper-style sheet with everything filled in: abilities, skills, combat, attacks, features, equipment and spells. In the print screen, choose ', h('b', null, 'Save as PDF'), ' to keep a copy or share it.'), h('label', { class: 'f' }, 'Paper size'), h('div', { class: 'seg' }, ['A4', 'Letter'].map(p => h('button', { 'aria-pressed': o.paper === p ? 'true' : 'false', onclick: () => { o.paper = p; fillSheet(rec); } }, p))), h('label', { class: 'f' }, 'Player name (optional)'), h('input', { class: 'in', value: o.player, maxlength: 40, oninput: e => o.player = e.target.value }), h('div', { class: 'mt' }, tog('spells', 'Include the spells page', 'Slots, save DC and every known spell'), tog('fullText', 'Include full rules text', 'Feature and spell descriptions in full (adds pages)'), tog('blankHP', 'Leave hit points, slots and death saves blank', 'For tracking in pencil at the table')), h('button', { class: 'btn ghost sm mt', onclick: () => { const w = h('div', { class: 'scrim', style: { zIndex: 80, alignItems: 'stretch', padding: 0 } }); const fr = h('iframe', { title: 'Preview', style: { flex: 1, border: 0, width: '100%', height: '100%', background: '#ece6dd' } }); fr.srcdoc = sheetPrintHTML(c, Object.assign({}, o, { portrait: ctx.portrait, fit: Math.min(1, (window.innerWidth - 10) / 820) })); w.append(h('div', { style: { display: 'flex', flexDirection: 'column', width: '100%', height: '100%' } }, h('div', { class: 'row', style: { padding: '10px 14px', background: 'var(--bg)' } }, h('b', { class: 'grow' }, 'Preview'), h('button', { class: 'btn sm', onclick: () => w.remove() }, 'Close')), fr)); document.body.append(w); } }, 'Preview')); }); } boot();