const PX = 96; // px per inch at screen
const LOGO = "https://beachpartyballoons.com/assets/logo/BeachPartyBalloons-logo.webp";
const FOLD = { w: 11*PX, h: 8.5*PX };
const FLAT = { w: 8.5*PX, h: 11*PX };
const MM = 96 / 25.4; // px per mm at screen
const PAPER_DIMS = {
letter: FLAT,
a4: { w: Math.round(210*MM), h: Math.round(297*MM) }
};
/* ---- Theme decorations (inline SVG, scale to fill their face) ---- */
function deco(theme, variant){
switch(theme.id){
case 'confetti':{
const c=['#FF5A7A','#FFC94D','#4DB6FF','#7BD389','#B07CFF'];
let s='';
const pts=[[8,10,0],[20,22,1],[33,8,2],[46,18,3],[60,9,4],[72,20,0],[86,12,1],[92,28,2],
[6,80,3],[18,90,4],[30,78,0],[44,88,1],[58,82,2],[70,92,3],[84,84,4],[94,76,0]];
pts.forEach((p,i)=>{
const col=c[p[2]];
if(i%3===0) s+=``;
else if(i%3===1) s+=``;
else s+=``;
});
return svg(s);
}
case 'gold':{
const a=theme.vars['--c-accent'];
const fr=`
`;
const fl=(x,y,r)=>`
`;
return svg(fr+fl(12,15,0)+fl(88,15,90)+fl(12,85,-90)+fl(88,85,180));
}
case 'botanical':{
const a=theme.vars['--c-accent'];
// leaves-outline-with-vines-svgrepo-com.svg path (viewBox 0 0 136.492 136.492)
const vp=`M121.036,78.928c-13.526-14.684-30.95-14.207-47.129-12.3c-0.375-0.216-0.802-0.34-1.266-0.292c-4.219,0.458-9.566-0.227-11.18-6.275c-0.937-3.562,1.187-7.235,4.725-8.174c2.624-0.709,5.332,0.878,6.027,3.493c0.491,1.867-0.622,3.789-2.476,4.282c-0.681,0.171-1.35,0.026-1.891-0.324c0.562-1.081,0.304-2.426-0.701-3.169c-1.118-0.849-2.7-0.612-3.536,0.511c-0.01,0.016-0.01,0.04-0.021,0.056c-0.768,0.496-1.179,1.419-0.937,2.354c0.947,3.541,4.622,5.69,8.182,4.757c4.158-1.103,6.643-5.39,5.545-9.563c-1.308-4.915-6.381-7.852-11.295-6.576c-5.843,1.553-9.336,7.586-7.796,13.461c0.793,2.987,2.386,5.323,4.588,6.966c-1.965,0.227-3.913,0.433-5.838,0.572c-19.478,1.506-30.855,1.822-38.333-9.207c-2.354-3.473-3.483-7.528-3.369-11.687c1.782,4.688,5.679,8.248,11.625,10.563c2.742,1.071,5.276,1.592,7.744,1.592l0,0c9.671,0,17.133-7.992,26.757-21.808c4.422-6.357,10.167-11.962,23.838-18.686c0.143-0.069,0.227-0.209,0.349-0.312c0.079-0.066,0.152-0.124,0.227-0.2c0.132-0.129,0.248-0.259,0.337-0.427c0.158-0.269,0.285-0.567,0.296-0.917c0.011-0.356-0.09-0.691-0.232-1c-0.037-0.097-0.1-0.15-0.143-0.232c-0.142-0.232-0.332-0.422-0.554-0.577c-0.053-0.037-0.068-0.098-0.115-0.114C83.399,15.051,58.057,0,35.935,0C21.926,0,12.332,5.957,7.417,17.708c-1.155,2.777-1.139,7.863,1.179,11.826c1.339,2.299,3.298,3.899,5.695,4.822c-1.804,2.716-3.13,5.746-3.789,8.976c-1.345,6.555-0.056,13.165,3.639,18.592c9.255,13.634,24.542,12.458,42.245,11.093c3.995-0.316,8.092-0.834,12.229-1.382c17.494-2.265,35.588-4.609,49.272,10.22c2.352,2.562,4.155,5.416,5.437,8.438c-0.005,0.116-0.047,0.211-0.042,0.327c0.69,10.515-3.617,17.323-12.145,19.147c-6.603,1.439-13.157-2.8-14.57-9.402c-1.076-5.073,2.151-10.072,7.208-11.148c1.852-0.385,3.724-0.053,5.311,0.971c1.588,1.023,2.685,2.605,3.074,4.472c0.27,1.234,0.026,2.474-0.563,3.565c-0.228-0.338-0.448-0.686-0.786-0.949c-1.756-1.461-4.366-1.203-5.822,0.563c-1.45,1.772-1.196,4.372,0.575,5.827c0.459,0.369,0.98,0.623,1.524,0.781c0.501,0.506,1.223,0.785,1.972,0.633c2.5-0.544,4.625-2.015,6.007-4.166c1.382-2.131,1.851-4.684,1.312-7.178c-0.643-2.99-2.409-5.537-4.967-7.193c-2.548-1.65-5.585-2.188-8.554-1.555c-7.378,1.565-12.103,8.869-10.531,16.284c1.683,7.763,8.569,13.089,16.211,13.089c1.149,0,2.325-0.111,3.49-0.364c6.318-1.351,10.827-4.941,13.337-10.235c-0.881,8.849-5.864,17.254-13.711,23.087c-5.759,4.282-12.852,6.075-19.955,5.021c-7.093-1.045-13.373-4.794-17.655-10.558c-0.712-0.955-2.062-1.149-3.029-0.443c-0.957,0.718-1.157,2.067-0.448,3.027c4.976,6.687,12.258,11.048,20.5,12.266c1.535,0.222,3.075,0.332,4.609,0.332c6.66,0,13.109-2.125,18.558-6.175c8.88-6.598,14.512-16.189,15.456-26.325C130.538,94.806,127.469,85.904,121.036,78.928z M20.945,7.966c2.17-1.284,4.627-2.226,7.338-2.824L23.637,24.83c-1.748,1.031-3.33,2.159-4.653,3.436l2.02-19.672C21.03,8.372,20.982,8.171,20.945,7.966z M39.099,54.288L20.252,33.816c1.152-1.205,2.399-2.31,3.813-3.22c0.432-0.274,0.704-0.691,0.854-1.139l19.765,21.065c-1.936,1.745-3.76,3.024-5.532,3.847C39.131,54.346,39.125,54.314,39.099,54.288z M33.705,55.653c-1.917,0-3.937-0.427-6.173-1.289c-4.777-1.87-7.873-4.602-9.191-8.124c-1.113-2.964-0.81-6.041-0.243-8.385L34.411,55.58C34.179,55.59,33.942,55.653,33.705,55.653L33.705,55.653z M47.734,47.466L28.589,27.061c1.859-0.864,3.889-1.653,6.088-2.339l17.65,17.216C50.704,44.055,49.174,45.895,47.734,47.466z M46.632,5.487c2.305,0.448,4.591,0.988,6.842,1.627L44.99,17.858c-1.695,0.309-3.386,0.633-5.044,1.016l6.61-13.115C46.606,5.672,46.601,5.579,46.632,5.487z M63.982,10.676l-6.265,5.218c-0.103,0.087-0.156,0.197-0.243,0.306c-2.041,0.189-4.124,0.427-6.215,0.704l6.637-8.419C60.003,9.176,62.028,9.906,63.982,10.676z M74.524,15.312c-2.816,0.037-6.146,0.135-9.774,0.338l3.746-3.114C70.71,13.5,72.751,14.441,74.524,15.312z M68.702,23.723l-3.932-3.736c3.629-0.216,7.069-0.335,10.181-0.361C72.573,21.018,70.526,22.383,68.702,23.723z M61.324,30.209l-9.746-8.996c2.468-0.33,4.936-0.588,7.38-0.805l6.315,6.015C63.771,27.715,62.466,28.978,61.324,30.209z M54.927,38.449L39.502,23.406c2.133-0.52,4.343-0.968,6.61-1.358l12.411,11.449c-0.567,0.741-1.107,1.463-1.605,2.193C56.222,36.69,55.584,37.536,54.927,38.449z M42.237,4.733l-7.862,15.593c-1.999,0.598-3.905,1.273-5.714,2.025l4.213-17.874c0.997-0.077,1.991-0.161,3.045-0.161C37.997,4.316,40.109,4.467,42.237,4.733z M12.31,27.369c-1.622-2.781-1.577-6.426-0.917-7.992c1.263-3.024,2.943-5.537,4.952-7.652l-1.843,17.969C13.615,29.112,12.882,28.34,12.31,27.369z`;
const vine=(tx,ty,r,s,op=0.3)=>``;
// Bottom-left and top-right opposing corners (vine SVG is 136.5×136.5, scaled to ~55 units)
return svg(vine(0,45,0,0.4) + vine(100,55,180,0.4));
}
case 'watercolor':{
const cols = ['#A389D4', '#7DADE2', '#E27DA1', '#7DE2C9', '#B87DE2', '#7D96E2'];
const blob = (x, y, rx, ry, r, col, op) => ``;
let s = '';
// Top-Left Cluster
s += blob(5, 5, 35, 28, 30, cols[0], 0.15);
s += blob(18, 8, 25, 20, -15, cols[1], 0.12);
s += blob(10, 22, 28, 22, 45, cols[2], 0.1);
s += blob(25, 15, 18, 15, 10, cols[4], 0.08);
// Top-Right Cluster
s += blob(95, 5, 32, 25, -30, cols[3], 0.14);
s += blob(82, 12, 24, 20, 20, cols[5], 0.12);
s += blob(90, 25, 20, 18, -45, cols[0], 0.09);
// Bottom-Left Cluster
s += blob(5, 95, 38, 24, 15, cols[1], 0.16);
s += blob(22, 88, 28, 18, -10, cols[4], 0.11);
s += blob(12, 80, 22, 16, 35, cols[3], 0.13);
// Bottom-Right Cluster
s += blob(95, 95, 34, 28, 45, cols[2], 0.17);
s += blob(78, 88, 25, 22, -25, cols[5], 0.12);
s += blob(88, 75, 20, 18, 15, cols[1], 0.1);
s += blob(98, 82, 15, 12, -10, cols[0], 0.08);
// Edge and Middle Washes
s += blob(50, 5, 45, 14, 0, cols[4], 0.08);
s += blob(50, 95, 50, 18, 5, cols[0], 0.07);
s += blob(5, 50, 12, 35, 0, cols[2], 0.06);
s += blob(95, 50, 14, 40, 0, cols[3], 0.06);
return svg(s);
}
case 'balloon':{
const cols=['#5A9AFF','#7AB5FF','#A0CFFF'];
const bal=(x,y,col,sc,r)=>`
`;
// Group of balloons tied together
const group=`
${bal(-8,-12,cols[0],1.2,10)}${bal(8,-10,cols[1],1.1,-10)}${bal(0,-20,cols[2],1.3,0)}
`;
// Floating sparkles
let sparkles='';
for(let i=0;i<12;i++) sparkles+=``;
return svg(sparkles + group);
}
case 'kraft':{
const a=theme.vars['--c-accent'];
const border=``;
const sprig=`
`;
return svg(border+sprig);
}
case 'flowers':{
const a=theme.vars['--c-accent'];
const img=(href,x,y,w,h,op=1)=>``;
const fg='assets/flower-green-svgrepo-com(1).svg';
const fl='assets/flower-leaf-2-svgrepo-com.svg';
const fb='assets/flower-bouquet-svgrepo-com(2).svg';
// Accent petals scattered near centre
let s='';
[[22,38],[78,35],[50,12],[50,86],[15,65],[85,62]].forEach(([x,y])=>{
s+=``;
});
s+=img(fb,8,18,84,72,0.07); // faint bouquet watermark behind text
s+=img(fg,-8,-8,52,52); // flower top-left
s+=img(fg,56,56,52,52); // flower bottom-right
s+=img(fl,54,-8,52,52,0.85); // leaf top-right
s+=img(fl,-8,56,52,52,0.85); // leaf bottom-left
return svg(s);
}
case 'retro':{
const a=theme.vars['--c-accent'];
const cy='#00EEFF';
let s='';
// Pixel stars
[[6,4],[19,7],[31,3],[47,9],[62,5],[77,8],[91,4],[12,15],[55,12],[83,14],
[3,25],[38,28],[70,23],[96,18],[25,35],[85,32]].forEach(([x,y],i)=>{
const col=i%3===0?cy:i%3===1?a:'#FFFFFF';
s+=``;
});
// Synthwave horizon sun
const hz=70;
s+=``;
s+=``;
[0,2.4,4.8].forEach(dy=>{ s+=``; });
// Perspective grid lines
for(let i=0;i<=6;i++)
s+=``;
for(let i=1;i<=4;i++){
const t=i/4, y=hz+t*(100-hz), hw=t*50;
s+=``;
}
// Space invader sprite (top-center, each pixel = 1.8 units)
const ps=1.8, ox=50-9*ps/2, oy=5;
[[0,0,1,0,0,0,1,0,0],
[0,0,0,1,1,1,0,0,0],
[0,1,1,1,1,1,1,1,0],
[1,1,0,1,1,1,0,1,1],
[1,1,1,1,1,1,1,1,1],
[0,1,0,0,0,0,0,1,0],
[1,0,1,0,0,0,1,0,1],
].forEach((row,r)=>row.forEach((on,c)=>{
if(on) s+=``;
}));
// Neon corner brackets
const BW=6, BT=1.2;
s+=``;
s+=``;
s+=``;
s+=``;
s+=``;
s+=``;
s+=``;
s+=``;
return svg(s);
}
}
return svg('');
}
function svg(inner){
return ``;
}
/* ---- Themes ---- */
const THEMES = [
{ id:'confetti', name:'Confetti', vars:{ '--c-bg':'#FFFBF2','--c-ink':'#2A2438','--c-accent':'#FF5A7A', '--c-titleFont':"'Fredoka', sans-serif",'--c-titleWeight':'600', '--c-titleSize':'48px','--c-titleLS':'-.01em','--c-titleCase':'none', '--c-bodyFont':"'Quicksand', sans-serif",'--c-subSize':'17px', }, chipBg:'#FFFBF2', chipInk:'#FF5A7A', chipGlyph:'★' },
{ id:'gold', name:'Midnight Gold', vars:{ '--c-bg':'#12243F','--c-ink':'#EDE3CB','--c-accent':'#C9A24B', '--c-titleFont':"'Cinzel', serif",'--c-titleWeight':'700', '--c-titleSize':'40px','--c-titleLS':'.08em','--c-titleCase':'uppercase', '--c-bodyFont':"'Lora', serif",'--c-subSize':'14px', '--c-subLS':'.22em','--c-subCase':'uppercase','--c-subWeight':'500', }, chipBg:'#12243F', chipInk:'#C9A24B', chipGlyph:'❧' },
{ id:'botanical', name:'Botanical', vars:{ '--c-bg':'#FDFCF9','--c-ink':'#2D362E','--c-accent':'#6B8E6B', '--c-titleFont':"'Playfair Display', serif",'--c-titleWeight':'700', '--c-titleSize':'46px','--c-titleLS':'0.02em','--c-titleCase':'none', '--c-bodyFont':"'Lora', serif",'--c-subSize':'18px','--c-subStyle':'italic', }, chipBg:'#FDFCF9', chipInk:'#6B8E6B', chipGlyph:'❀' },
{ id:'watercolor', name:'Watercolor', vars:{ '--c-bg':'#FCF9FF','--c-ink':'#3D2B52','--c-accent':'#A389D4', '--c-titleFont':"'Dancing Script', cursive",'--c-titleWeight':'700', '--c-titleSize':'52px','--c-titleLS':'0','--c-titleCase':'none', '--c-bodyFont':"'Raleway', sans-serif",'--c-subSize':'16px', '--c-subLS':'.05em','--c-subWeight':'500', }, chipBg:'#FCF9FF', chipInk:'#A389D4', chipGlyph:'༄' },
{ id:'balloon', name:'Balloons', vars:{ '--c-bg':'#F7FBFF','--c-ink':'#2A4B6B','--c-accent':'#5A9AFF', '--c-titleFont':"'Fredoka', sans-serif",'--c-titleWeight':'600', '--c-titleSize':'48px','--c-titleLS':'-.01em', '--c-bodyFont':"'Quicksand', sans-serif",'--c-subSize':'17px', }, chipBg:'#F7FBFF', chipInk:'#5A9AFF', chipGlyph:'🎈' },
{ id:'kraft', name:'Kraft Note', vars:{ '--c-bg':'#D7C3A3','--c-ink':'#41301F','--c-accent':'#8A5A38', '--c-titleFont':"'Caveat', cursive",'--c-titleWeight':'700', '--c-titleSize':'42px','--c-titleLS':'0', '--c-bodyFont':"monospace",'--c-subSize':'16px','--c-subLS':'0.04em', }, chipBg:'#D7C3A3', chipInk:'#8A5A38', chipGlyph:'✶' },
{ id:'flowers', name:'Garden Blooms', vars:{ '--c-bg':'#FEFCF6','--c-ink':'#273A1C','--c-accent':'#D4517A', '--c-titleFont':"'Dancing Script', cursive",'--c-titleWeight':'700', '--c-titleSize':'56px','--c-titleLS':'0.01em','--c-titleCase':'none', '--c-bodyFont':"'Lora', serif",'--c-subSize':'17px','--c-subStyle':'italic', }, chipBg:'#FEFCF6', chipInk:'#D4517A', chipGlyph:'✿' },
{ id:'retro', name:'Retro Arcade', vars:{ '--c-bg':'#0D0A1E','--c-ink':'#00EEFF','--c-accent':'#FF1177', '--c-titleFont':"'Press Start 2P', monospace",'--c-titleWeight':'400', '--c-titleSize':'20px','--c-titleLS':'0.04em','--c-titleCase':'uppercase', '--c-bodyFont':"monospace",'--c-subSize':'10px','--c-subLS':'0.12em','--c-subCase':'uppercase','--c-subWeight':'400', }, chipBg:'#0D0A1E', chipInk:'#FF1177', chipGlyph:'👾' },
];
const PRESETS = [
{ name:"Birthday", title:"Happy Birthday", sub:"Wishing you a wonderful day", msg:"Hope your day is filled with joy, laughter, and a little extra cake.\n\nWith love,", font:"’Fredoka’, sans-serif" },
{ name:"Thank You", title:"Thank You", sub:"with heartfelt gratitude", msg:"Thank you so much for your kindness — it truly meant the world to me.\n\nWarmly,", font:"’Dancing Script’, cursive" },
{ name:"Congrats", title:"Congratulations!", sub:"on your wonderful news", msg:"So thrilled for you and everything you’ve accomplished. Here’s to what comes next!", font:"’Montserrat’, sans-serif" },
{ name:"Invitation", title:"You’re Invited", sub:"Saturday · 4:00 PM", msg:"Join us to celebrate!\n\nPlease RSVP by [date]\n[Address]", font:"’Cinzel’, serif" },
{ name:"Thinking of You", title:"Thinking of You", sub:"sending warm wishes", msg:"Just a little note to let you know you’re in my thoughts today.", font:"’Lora’, serif" },
{ name:"Get Well", title:"Get Well Soon", sub:"feel better", msg:"Wishing you rest, comfort, and a speedy recovery.", font:"’Caveat’, cursive" },
{ name:"Anniversary", title:"Happy Anniversary", sub:"celebrating you both", msg:"Wishing you another year filled with love, laughter, and beautiful memories together.\n\nWith love,", font:"’Playfair Display’, serif" },
{ name:"Baby Shower", title:"Baby Shower", sub:"a new little one is on the way!", msg:"Wishing you all the joy and wonder of welcoming your new arrival.\n\nWith love,", font:"’Quicksand’, sans-serif" },
{ name:"Sympathy", title:"With Deepest Sympathy", sub:"in your time of loss", msg:"Please know that you are in my thoughts and heart during this difficult time.\n\nWith love,", font:"’Lora’, serif" },
{ name:"Graduation", title:"Congratulations, Graduate!", sub:"the world is yours", msg:"So proud of everything you’ve accomplished. This is just the beginning of an incredible journey!\n\nWith love,", font:"’Montserrat’, sans-serif" },
{ name:"Valentine’s", title:"Happy Valentine’s Day", sub:"with all my love", msg:"Every day with you is a gift. Thank you for being my favorite person.\n\nAll my love,", font:"’Dancing Script’, cursive" },
{ name:"Holiday", title:"Happy Holidays", sub:"wishing you joy & peace", msg:"May this season bring warmth, laughter, and all the things that make your heart happy.\n\nWith love,", font:"’Cinzel’, serif" },
];
/* ---- State ---- */
const state = {
style:'quarter', paper:'letter', theme:THEMES[0], title:'Happy Birthday', sub:'Wishing you a wonderful day',
msg:'Hope your day is filled with joy, laughter, and a little extra cake.\n\nWith love,', back:'',
logo:true, logoPos:'back', inside:false, guide:true, accent:null, text:null, bg:null, fontOverride:'',
doubleSided: false, activeSide: 'outside',
titleScale: 1, bodyScale: 1, align: 'center', msgSide: 'right'
};
/* ---- State Persistence ---- */
function saveState(){
const data = { ...state, themeId: state.theme.id };
delete data.theme; // Don't store the whole object reference
localStorage.setItem('cardStudioState', JSON.stringify(data));
}
function loadState(){
const saved = localStorage.getItem('cardStudioState');
if(!saved) return;
try {
const data = JSON.parse(saved);
const theme = THEMES.find(t => t.id === data.themeId) || THEMES[0];
Object.assign(state, data, { theme });
syncUI();
} catch(e) { console.error("Failed to load state", e); }
}
function syncUI(){
$('#fTitle').value = state.title;
$('#fSub').value = state.sub;
$('#fMsg').value = state.msg;
$('#fBack').value = state.back;
$('#fFont').value = state.fontOverride;
const ts = Math.round((state.titleScale || 1) * 100);
const bs = Math.round((state.bodyScale || 1) * 100);
$('#fTitleScale').value = ts; $('#fTitleScaleVal').textContent = ts + '%';
$('#fBodyScale').value = bs; $('#fBodyScaleVal').textContent = bs + '%';
$('#cInside').checked = state.inside;
$('#cDouble').checked = state.doubleSided;
$('#cGuide').checked = state.guide;
$('#cLogo').checked = state.logo;
$('#cAccent').value = toHex(state.accent || state.theme.vars['--c-accent']);
$('#cText').value = toHex(state.text || state.theme.vars['--c-ink']);
// Update segments
document.querySelectorAll('#paperSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.paper === (state.paper || 'letter')));
document.querySelectorAll('#styleSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.style === state.style));
document.querySelectorAll('#logoSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.pos === state.logoPos));
document.querySelectorAll('#sideSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.side === state.activeSide));
document.querySelectorAll('#alignSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.align === (state.align || 'center')));
document.querySelectorAll('#msgSideSeg button').forEach(b => b.setAttribute('aria-pressed', b.dataset.msgside === (state.msgSide || 'right')));
$('#msgSideWrap').style.display = state.style === 'quarter' ? '' : 'none';
document.querySelectorAll('.swatch').forEach(s => s.setAttribute('aria-pressed', s.dataset.id === state.theme.id));
// Visibility logic
const isFolded = (state.style === 'folded'); // Legacy support
if (state.style === 'folded') state.style = 'quarter'; // Migration
$('#optGroup').style.display = (state.style === 'folded') ? 'flex' : 'none'; // Only for legacy folded
$('#printGroup').style.display = (state.inside && state.style === 'folded') ? '' : 'none';
$('#sideSelector').style.display = state.doubleSided ? 'none' : '';
$('#logoPosWrap').style.display = (state.logo && state.style !== 'flat') ? '' : 'none';
document.body.classList.toggle('manual-flip', !state.doubleSided && state.inside && state.style === 'folded');
const notes={
quarter:'Quarter-fold: one portrait sheet, folded twice, makes a small 4.25 × 5.5" card — front, back, and inside message all print on this one sheet.',
flat:'Flat: four identical cards on one portrait sheet. Cut along the dashed lines — no folding. Great for invitations and thank-you cards.'
};
$('#styleNote').innerHTML = notes[state.style] || notes.quarter;
}
/* ---- Build controls ---- */
const $ = s=>document.querySelector(s);
const sheets = $('#sheets');
const swWrap=$('#swatches');
THEMES.forEach(t=>{
const b=document.createElement('button');
b.className='swatch';b.type='button';b.dataset.id=t.id;
b.setAttribute('aria-pressed', t.id===state.theme.id);
b.innerHTML=`${t.chipGlyph}${t.name}`;
b.addEventListener('click',()=>{
state.theme=t; state.accent=null; state.text=null; syncColorInputs();
document.querySelectorAll('.swatch').forEach(s=>s.setAttribute('aria-pressed', s.dataset.id===t.id));
render();
});
swWrap.appendChild(b);
});
const pWrap=$('#presets');
PRESETS.forEach(p=>{
const b=document.createElement('button');
b.className='preset';b.type='button';b.textContent=p.name;
b.addEventListener('click',()=>{
state.title=p.title;state.sub=p.sub;state.msg=p.msg;
if(p.font) {
state.fontOverride=p.font;
$('#fFont').value=p.font;
}
$('#fTitle').value=p.title;$('#fSub').value=p.sub;$('#fMsg').value=p.msg;
render();
});
pWrap.appendChild(b);
});
$('#fTitle').addEventListener('input',e=>{state.title=e.target.value;render();});
$('#fSub').addEventListener('input',e=>{state.sub=e.target.value;render();});
$('#fMsg').addEventListener('input',e=>{state.msg=e.target.value;render();});
$('#msgSideSeg').addEventListener('click',e=>{
const b=e.target.closest('button');
if(!b) return;
state.msgSide=b.dataset.msgside;
document.querySelectorAll('#msgSideSeg button').forEach(x=>x.setAttribute('aria-pressed', x===b));
render();
});
$('#fBack').addEventListener('input',e=>{state.back=e.target.value;render();});
$('#fFont').addEventListener('change',e=>{state.fontOverride=e.target.value;render();});
$('#alignSeg').addEventListener('click',e=>{
const b=e.target.closest('button');
if(!b) return;
state.align=b.dataset.align;
document.querySelectorAll('#alignSeg button').forEach(x=>x.setAttribute('aria-pressed', x===b));
render();
});
$('#fTitleScale').addEventListener('input',e=>{
state.titleScale = e.target.value / 100;
$('#fTitleScaleVal').textContent = e.target.value + '%';
render();
});
$('#fBodyScale').addEventListener('input',e=>{
state.bodyScale = e.target.value / 100;
$('#fBodyScaleVal').textContent = e.target.value + '%';
render();
});
$('#cInside').addEventListener('change',e=>{
state.inside=e.target.checked;
$('#printGroup').style.display = (state.inside && state.style==='folded') ? '' : 'none';
render();
});
$('#cDouble').addEventListener('change',e=>{
state.doubleSided=e.target.checked;
$('#sideSelector').style.display = state.doubleSided ? 'none' : '';
document.body.classList.toggle('manual-flip', !state.doubleSided && state.inside);
render();
});
$('#sideSeg').addEventListener('click', e => {
const b = e.target.closest('button');
if(!b) return;
state.activeSide = b.dataset.side;
document.querySelectorAll('#sideSeg button').forEach(x => x.setAttribute('aria-pressed', x === b));
render();
});
$('#paperSeg').addEventListener('click',e=>{
const b=e.target.closest('button');
if(!b) return;
state.paper=b.dataset.paper;
document.querySelectorAll('#paperSeg button').forEach(x=>x.setAttribute('aria-pressed', x===b));
render();
});
$('#styleSeg').addEventListener('click',e=>{
const btn=e.target.closest('button');
if(!btn) return;
state.style=btn.dataset.style;
document.querySelectorAll('#styleSeg button').forEach(b=>b.setAttribute('aria-pressed', b===btn));
const s=state.style;
const notes={
quarter:'Quarter-fold: one portrait sheet, folded twice, makes a small 4.25 × 5.5" card — front, back, and inside message all print on this one sheet.',
flat:'Flat: four identical cards on one portrait sheet. Cut along the dashed lines — no folding. Great for invitations and thank-you cards.'
};
$('#styleNote').innerHTML = notes[s];
const isFolded = (s === 'folded');
$('#printGroup').style.display = (state.inside && isFolded) ? '' : 'none';
const cut = (s==='flat');
$('#guideOptLabel').firstChild.textContent = cut ? 'Show cut lines when printing' : 'Show fold lines when printing';
$('#guideOptSub').textContent = cut ? 'Faint dashed lines to trim along.' : 'Faint dashed creases to fold against.';
$('#msgLabel').textContent = (s==='flat') ? 'Card message' : 'Inside message';
$('#msgHint').textContent = s==='flat' ? 'Shown on each flat card.' : s==='quarter'? 'Printed inside the small card (same sheet).' : 'Used on the inside page (enable below) — and shown on flat cards.';
$('#msgSideWrap').style.display = s === 'quarter' ? '' : 'none';
syncLogoUI();
render();
});
$('#cGuide').addEventListener('change',e=>{state.guide=e.target.checked;render();});
function syncLogoUI(){
$('#logoPosWrap').style.display = (state.logo && state.style!=='flat') ? '' : 'none';
}
$('#cLogo').addEventListener('change',e=>{state.logo=e.target.checked;syncLogoUI();render();});
$('#logoSeg').addEventListener('click',e=>{
const b=e.target.closest('button');
if(!b) return;
state.logoPos=b.dataset.pos;
document.querySelectorAll('#logoSeg button').forEach(x=>x.setAttribute('aria-pressed',x===b));
render();
});
function syncColorInputs(){
const t=state.theme.vars;
$('#cAccent').value = toHex(state.accent || t['--c-accent']);
$('#cText').value = toHex(state.text || t['--c-ink']);
$('#cBg').value = toHex(state.bg || t['--c-bg']);
}
function toHex(c){
if(/^#([0-9a-f]{6})$/i.test(c)) return c;
return '#000000';
}
$('#cAccent').addEventListener('input',e=>{state.accent=e.target.value;render();});
$('#cText').addEventListener('input',e=>{state.text=e.target.value;render();});
$('#cBg').addEventListener('input',e=>{state.bg=e.target.value;render();});
$('#resetColors').addEventListener('click',()=>{state.accent=null;state.text=null;state.bg=null;syncColorInputs();render();});
$('#printBtn').addEventListener('click',()=>window.print());
window.addEventListener('beforeprint',()=>{
document.querySelectorAll('.sheet').forEach(s=>s.style.transform='none');
// Hide images still loading — Chrome hangs on "Preparing preview" if any image fetch is pending
document.querySelectorAll('.sheet img').forEach(img=>{if(!img.complete)img.style.display='none';});
});
window.addEventListener('afterprint',()=>{
document.querySelectorAll('.sheet img').forEach(img=>img.style.display='');
fit();
});
/* ---- Rendering ---- */
function faceVars(){
const v={...state.theme.vars};
if(state.accent) v['--c-accent']=state.accent;
if(state.text) v['--c-ink']=state.text;
if(state.bg) v['--c-bg']=state.bg;
const itemsMap = { left:'flex-start', center:'center', right:'flex-end' };
v['--c-align'] = state.align || 'center';
v['--c-items'] = itemsMap[state.align || 'center'];
if(state.fontOverride) v['--c-titleFont']=state.fontOverride;
const ts = state.titleScale || 1;
if(ts !== 1){
const base = parseFloat(v['--c-titleSize'] || '40px');
v['--c-titleSize'] = (base * ts).toFixed(1) + 'px';
}
const bs = state.bodyScale || 1;
if(bs !== 1){
const subBase = parseFloat(v['--c-subSize'] || '16px');
v['--c-subSize'] = (subBase * bs).toFixed(1) + 'px';
const msgBase = parseFloat(v['--c-msgSize'] || '15px');
v['--c-msgSize'] = (msgBase * bs).toFixed(1) + 'px';
}
return Object.entries(v).map(([k,val])=>`${k}:${val}`).join(';');
}
function esc(s){return (s||'').replace(/&/g,'&').replace(//g,'>');}
function logoImg(cls){
return `

`;
}
function coverFace(){
const front = state.logo && state.logoPos==='front';
return `
${deco(state.theme,'cover')}
${state.title?`
${esc(state.title)}
`:''}
${state.sub?`
${esc(state.sub)}
`:''}
${front?logoImg('logo-sm'):''}
`;
}
function backFace(){
const back = state.logo && state.logoPos==='back';
return `
${deco(state.theme,'back')}
${back?`
${logoImg('')}${state.back?`
${esc(state.back)}
`:''}
`
:(state.back?`
${esc(state.back)}
`:'')}
`;
}
function insideFace(side){
const hasMsg = (side === (state.msgSide || 'right'));
if(hasMsg){
return ``;
}
return ``;
}
function flatCell(){
return `
${deco(state.theme,'cover')}
${state.title?`
${esc(state.title)}
`:''}
${state.sub?`
${esc(state.sub)}
`:''}
${state.msg?`
${esc(state.msg)}
`:''}
${state.logo?logoImg('logo-sm'):''}
`;
}
function render(){
let html='';
if(state.style==='folded'){
const manual = !state.doubleSided && state.inside;
const isOutside = !manual || state.activeSide === 'outside';
const isInside = !manual || state.activeSide === 'inside';
if(isOutside){
const outside = `
${backFace()}
${coverFace()}
fold
`;
html += `${holderLabel(state.inside?'Outside (front & back)':'Card sheet — fold in half', outside)}
`;
}
if(state.inside && isInside){
const inside = `
${insideFace('left')}
${insideFace('right')}
fold
`;
html += `${holderLabel('Inside (message)', inside)}
`;
}
} else if(state.style==='quarter'){
const g = `
${insideFace('left')}
${insideFace('right')}
${backFace()}
${coverFace()}
`;
const labels = `
Inside ↧ (prints upside-down)Message ↧
BackFront cover
`;
const sheet = ``;
html += `${holderLabel('Small card — fold in half, then half again', sheet)}
`;
} else {
const flat = `
${flatCell()}${flatCell()}${flatCell()}${flatCell()}
`;
html += `${holderLabel('4 cards — cut to separate', flat)}
`;
}
sheets.innerHTML = html;
updatePageStyle();
fit();
saveState();
}
function holderLabel(label, inner){
return `${label}
${inner}`;
}
const guideClass = ()=> 'guide ' + (state.guide ? '' : 'noprint-guide');
let pageStyleEl=document.createElement('style');document.head.appendChild(pageStyleEl);
function updatePageStyle(){
const land = state.style==='folded';
const orient = land ? 'landscape' : 'portrait';
const pageSize = state.paper === 'a4' ? 'A4' : 'Letter';
const sheetW = state.paper === 'a4' ? '210mm' : '8.5in';
const sheetH = state.paper === 'a4' ? '297mm' : '11in';
pageStyleEl.textContent = `@page{ size: ${pageSize} ${orient}; margin:0; }
.sheet.flat,.sheet.quarter{ width:${sheetW}; height:${sheetH}; }`;
}
function fit(){
const dim = PAPER_DIMS[state.paper || 'letter'];
const wrap = document.getElementById('previewWrap');
if(!wrap) return;
// Use a more robust width measurement
const isMobile = window.innerWidth <= 960;
const w = (isMobile ? window.innerWidth : wrap.clientWidth) || 360;
const h = wrap.clientHeight || (window.innerHeight * 0.5);
const style = window.getComputedStyle(wrap);
const padW = (parseFloat(style.paddingLeft)||0) + (parseFloat(style.paddingRight)||0);
const padH = (parseFloat(style.paddingTop)||0) + (parseFloat(style.paddingBottom)||0);
// Leave a bit more breathing room on mobile
const margin = isMobile ? 24 : 12;
const availW = Math.max(40, w - padW - margin);
const availH = Math.max(40, h - padH - margin);
let scale = Math.min(1, availW / dim.w, availH / dim.h);
if (isNaN(scale) || scale <= 0) scale = 0.1;
document.querySelectorAll('.sheetHolder').forEach(holder => {
const sheet = holder.querySelector('.sheet');
if(sheet) sheet.style.transform = `scale(${scale})`;
holder.style.width = Math.floor(dim.w * scale) + 'px';
holder.style.height = Math.floor(dim.h * scale) + 'px';
});
}
window.addEventListener('resize',fit);
window.addEventListener('load',fit);
window.addEventListener('orientationchange',()=>setTimeout(fit,200));
if('ResizeObserver' in window){
new ResizeObserver(fit).observe(document.getElementById('previewWrap'));
}
/* ---- Tabs ---- */
document.getElementById('tabBar').addEventListener('click', e => {
const btn = e.target.closest('.tab');
if (!btn) return;
const target = btn.dataset.tab;
document.querySelectorAll('.tab').forEach(t => {
t.classList.toggle('active', t === btn);
t.setAttribute('aria-selected', t === btn);
});
document.querySelectorAll('.tab-panel').forEach(p => {
p.classList.toggle('active', p.dataset.tab === target);
});
requestAnimationFrame(fit);
});
loadState();
syncColorInputs();
syncLogoUI();
render();
requestAnimationFrame(fit);