From fb03a99f2c8ef3b36bea209555806ed8eff1119a Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 16 Jun 2026 21:33:49 -0400 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Card=20Studio=20?= =?UTF-8?q?printable=20card=20generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Features: quarter-fold and flat 4-up layouts, 8 themes, 12 presets, font picker, text alignment, text size sliders, background/accent/text colour pickers, message page (left/right) toggle, A4/Letter paper size, tabbed sidebar UI, responsive mobile layout, localStorage persistence, and Beach Party Balloons branding support. Co-Authored-By: Claude Sonnet 4.6 --- assets/flower-bouquet-svgrepo-com(2).svg | 15 + assets/flower-green-svgrepo-com(1).svg | 21 + assets/flower-leaf-2-svgrepo-com.svg | 20 + .../leaves-outline-with-vines-svgrepo-com.svg | 49 ++ index.html | 212 ++++++ script.js | 640 ++++++++++++++++++ style.css | 362 ++++++++++ 7 files changed, 1319 insertions(+) create mode 100644 assets/flower-bouquet-svgrepo-com(2).svg create mode 100644 assets/flower-green-svgrepo-com(1).svg create mode 100644 assets/flower-leaf-2-svgrepo-com.svg create mode 100644 assets/leaves-outline-with-vines-svgrepo-com.svg create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/assets/flower-bouquet-svgrepo-com(2).svg b/assets/flower-bouquet-svgrepo-com(2).svg new file mode 100644 index 0000000..748b67e --- /dev/null +++ b/assets/flower-bouquet-svgrepo-com(2).svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/flower-green-svgrepo-com(1).svg b/assets/flower-green-svgrepo-com(1).svg new file mode 100644 index 0000000..d69c84c --- /dev/null +++ b/assets/flower-green-svgrepo-com(1).svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + Flower abstract pecah + + + + + \ No newline at end of file diff --git a/assets/flower-leaf-2-svgrepo-com.svg b/assets/flower-leaf-2-svgrepo-com.svg new file mode 100644 index 0000000..ec1edbd --- /dev/null +++ b/assets/flower-leaf-2-svgrepo-com.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + Flower abstract pecah + + + + \ No newline at end of file diff --git a/assets/leaves-outline-with-vines-svgrepo-com.svg b/assets/leaves-outline-with-vines-svgrepo-com.svg new file mode 100644 index 0000000..e5f256b --- /dev/null +++ b/assets/leaves-outline-with-vines-svgrepo-com.svg @@ -0,0 +1,49 @@ + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..03568a6 --- /dev/null +++ b/index.html @@ -0,0 +1,212 @@ + + + + + +Card Studio — Printable Cards + + + + + + + +
+
+

Card Studio

+ printable cards +
+ +
+ +
+ + +
+
+
+
+
+
+ + + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..20de442 --- /dev/null +++ b/script.js @@ -0,0 +1,640 @@ +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 `${inner}`; +} + +/* ---- 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 `
Beach Party Balloons
`; +} + +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 `
+

${esc(state.msg)}

+
`; + } + 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 = `
+ ${g} +
+
+ ${labels} +
`; + 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); diff --git a/style.css b/style.css new file mode 100644 index 0000000..a3fff97 --- /dev/null +++ b/style.css @@ -0,0 +1,362 @@ +:root{ + --bench:#e9e7e0; + --panel:#ffffff; + --ink:#23262b; + --muted:#6b6f76; + --line:#dcd9d1; + --accent:#3b5bdb; + --accent-soft:#e7ecfb; + --radius:12px; + --chrome-font:"Inter","Segoe UI",-apple-system,system-ui,Roboto,Helvetica,Arial,sans-serif; +} +*{box-sizing:border-box;} +html{ + margin:0;padding:0; + overflow-x:clip; /* clip never creates a scroll container, so sticky still works */ +} +body{ + margin:0;padding:0; + width:100%;max-width:100vw; + overflow-x:hidden; +} +body{ + font-family:var(--chrome-font); + color:var(--ink); + background:var(--bench); + -webkit-font-smoothing:antialiased; +} + +/* ---------- App chrome ---------- */ +header.topbar{ + display:flex;align-items:center;justify-content:space-between; + gap:16px;padding:14px 22px; + background:var(--panel); + border-bottom:1px solid var(--line); + position:sticky;top:0;z-index:30; +} +.brand{display:flex;align-items:baseline;gap:10px; min-width: 0;} +.brand h1{font-size:17px;font-weight:650;letter-spacing:-.01em;margin:0; white-space: nowrap;} +.brand .tag{font-size:12px;color:var(--muted);letter-spacing:.02em; white-space: nowrap;} +.btn{ + font:inherit;font-size:13px;font-weight:550; + border:1px solid var(--line);background:var(--panel);color:var(--ink); + padding:9px 14px;border-radius:9px;cursor:pointer; + transition:background .15s,border-color .15s,transform .05s; + white-space: nowrap; +} +.btn:hover{background:#f6f5f1;} +.btn:active{transform:translateY(1px);} +.btn.primary{background:var(--accent);border-color:var(--accent);color:#fff;} +.btn.primary:hover{background:#3450c4;} +.btn:focus-visible,.swatch:focus-visible,input:focus-visible,textarea:focus-visible,.seg button:focus-visible{ + outline:2px solid var(--accent);outline-offset:2px; +} +.layout{ + display:flex;align-items:flex-start;gap:0; + width: 100%; + max-width: 100vw; +} + +/* ---------- Controls ---------- */ +.controls{ + width:330px;flex:0 0 330px; + background:var(--panel); + border-right:1px solid var(--line); + height:calc(100vh - 57px); + overflow-y:auto; + overflow-x:hidden; + padding:0 0 60px; +} + +/* Tabs */ +.tabs{ + display:flex; + position:sticky;top:0;z-index:10; + background:var(--panel); + border-bottom:1px solid var(--line); + padding:0 4px; +} +.tab{ + flex:1; + font:inherit;font-size:13px;font-weight:550; + color:var(--muted); + background:none;border:none; + border-bottom:2px solid transparent; + padding:12px 6px; + cursor:pointer; + transition:color .15s,border-color .15s; + margin-bottom:-1px; +} +.tab:hover{color:var(--ink);} +.tab.active{color:var(--accent);border-bottom-color:var(--accent);} +.tab-panel{display:none;padding:20px 20px 0;} +.tab-panel.active{display:block;} +.group{margin-bottom:24px; width: 100%;} +.group > .lbl{ + display:block;font-size:11px;font-weight:650;text-transform:uppercase; + letter-spacing:.09em;color:var(--muted);margin-bottom:10px; +} +.seg{display:flex;background:#f1efe9;border:1px solid var(--line);border-radius:9px;padding:3px;gap:3px; width:100%;} +.seg button{ + flex:1; min-width: 0; + font:inherit;font-size:12.5px;font-weight:550;color:var(--muted); + border:0;background:transparent;padding:7px 5px;border-radius:6px;cursor:pointer;line-height:1.15; +} +.seg button[aria-pressed="true"]{background:var(--panel);color:var(--ink);box-shadow:0 1px 2px rgba(0,0,0,.08);} + +.swatches{display:grid;grid-template-columns:repeat(3,1fr);gap:8px; width:100%;} +.swatch{ + border:2px solid transparent;border-radius:10px;padding:0;cursor:pointer; + background:none;text-align:center; +} +.swatch .chip{ + height:46px;border-radius:8px;border:1px solid rgba(0,0,0,.12); + display:flex;align-items:center;justify-content:center;overflow:hidden; +} +.swatch .chip span{font-size:18px;font-weight:700;} +.swatch .nm{display:block;font-size:11px;color:var(--muted);margin-top:5px;} +.swatch[aria-pressed="true"]{border-color:var(--accent);} +.swatch[aria-pressed="true"] .nm{color:var(--ink);font-weight:600;} + +.presets{display:flex;flex-wrap:wrap;gap:7px;} +.preset{ + font:inherit;font-size:12px;font-weight:540; + border:1px solid var(--line);background:var(--panel);color:var(--ink); + padding:6px 11px;border-radius:999px;cursor:pointer; +} +.preset:hover{background:var(--accent-soft);border-color:var(--accent);} + +.field{margin-bottom:13px; width: 100%;} +.field label{display:block;font-size:12px;font-weight:550;margin-bottom:5px;} +.field input,.field textarea{ + width:100%; max-width: 100%; font:inherit;font-size:13px;color:var(--ink); + border:1px solid var(--line);border-radius:8px;padding:9px 11px;background:#fcfbf8; + resize:vertical; +} +.field textarea{min-height:70px;line-height:1.45;} +.field .hint{font-size:11px;color:var(--muted);margin-top:4px;} + +.opt{display:flex;align-items:flex-start;gap:9px;margin-bottom:11px;font-size:13px;line-height:1.4; width: 100%;} +.opt input{margin-top:2px;width:15px;height:15px;accent-color:var(--accent); flex-shrink: 0;} +.opt > span { flex: 1; min-width: 0; } +.opt .sub{display:block;font-size:11px;color:var(--muted);} + +input[type=range]{width:100%;accent-color:var(--accent);cursor:pointer;margin-top:3px;display:block;} +.field label .slider-val{font-weight:400;color:var(--muted);margin-left:4px;} + +.colorrow{display:flex;gap:14px 18px;align-items:center;flex-wrap:wrap;} +.colorrow .ci{display:flex;align-items:center;gap:7px;font-size:12px;} +.colorrow input[type=color]{width:30px;height:30px;border:1px solid var(--line);border-radius:7px;padding:0;background:none;cursor:pointer;} +.reset-link{background:none;border:0;color:var(--accent);font:inherit;font-size:12px;cursor:pointer;padding:0;text-decoration:underline;} + +.note{ + font-size:12px;line-height:1.5;color:var(--muted); + background:#f6f5f1;border:1px solid var(--line);border-radius:9px;padding:11px 13px; + width: 100%; +} +.note b{color:var(--ink);font-weight:600;} + +/* ---------- Preview / workbench ---------- */ +.previewOuter{ + flex:1;min-width:0; + height:calc(100vh - 57px); +} +.previewWrap{ + width:100%;height:100%; + overflow-y:auto; overflow-x:hidden; + padding:34px; + display:flex;flex-direction:column;align-items:center;gap:26px; + background: radial-gradient(circle at 50% 0,#f2f0ea,transparent 60%), var(--bench); +} +.sheetHolder{ + position:relative; + margin: 0 auto 10px; +} +.sheetLabel{ + position:absolute;top:-26px;left:50%;transform:translateX(-50%); + font-size:11px;font-weight:600; + letter-spacing:.05em;text-transform:uppercase;color:var(--muted); + white-space:nowrap; +} +.sheet{ + position:absolute; top:0; left:0; + background:#fff;overflow:hidden; + transform-origin:top left; + box-shadow:0 10px 30px rgba(0,0,0,.16),0 2px 6px rgba(0,0,0,.08); + -webkit-print-color-adjust:exact;print-color-adjust:exact; + color:var(--c-ink,#222); +} +.sheet.folded{width:11in;height:8.5in;} +.sheet.flat{width:8.5in;height:11in;} +.sheet.quarter{width:8.5in;height:11in;} + +.face.inside-l{background:#fff;} +.face.inside-r{background:#fff;justify-content:flex-start;padding-top:1in;} +.face.inside-r .content{gap:.25in;} + +/* fold + cut guides (screen + optional print) */ +.guide{position:absolute;pointer-events:none;z-index:6;} +.guide.v{top:0;bottom:0;border-left:1.5px dashed rgba(0,0,0,.28);} +.guide.h{left:0;right:0;border-top:1.5px dashed rgba(0,0,0,.28);} +.guide.hide{display:none;} +.guide .gtxt{ + position:absolute;top:50%;left:6px;transform:translateY(-50%) rotate(0); + font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:rgba(0,0,0,.35); + font-family:var(--chrome-font);background:rgba(255,255,255,.6);padding:1px 4px;border-radius:3px; +} + +/* panels inside a folded sheet */ +.panel{position:absolute;top:0;bottom:0;display:flex;} +.panel.left{left:0;right:50%;} +.panel.right{left:50%;right:0;} + +/* the actual designed card face */ +.face{ + position:absolute;inset:0; + display:flex;flex-direction:column;align-items:var(--c-items,center);justify-content:center; + text-align:var(--c-align,center);padding:0.55in 0.5in; + background:var(--c-bg,#fff); +} +.face .deco{position:absolute;inset:0;z-index:0;pointer-events:none;} +.face .content{position:relative;z-index:2;max-width:100%;display:flex;flex-direction:column;align-items:var(--c-items,center);gap:.18in;} + +.ttl{ + font-family:var(--c-titleFont); + font-weight:var(--c-titleWeight,700); + font-size:var(--c-titleSize,40px); + line-height:1.04;letter-spacing:var(--c-titleLS,-.01em); + color:var(--c-accent,#333); + margin:0;text-transform:var(--c-titleCase,none); + word-break:break-word; +} +.sub{ + font-family:var(--c-bodyFont); + font-size:var(--c-subSize,16px);font-weight:var(--c-subWeight,400); + line-height:1.4;color:var(--c-ink,#444);margin:0; + letter-spacing:var(--c-subLS,0);font-style:var(--c-subStyle,normal); + text-transform:var(--c-subCase,none); +} +.msg{ + font-family:var(--c-bodyFont); + font-size:var(--c-msgSize,15px);line-height:1.55;color:var(--c-ink,#444); + margin:0;white-space:pre-wrap; +} +.backmark{ + position:absolute;left:0;right:0;bottom:.45in;text-align:center;z-index:2; + font-family:var(--c-bodyFont);font-size:10px;letter-spacing:.14em; + text-transform:uppercase;color:var(--c-ink,#888);opacity:.55; +} + +/* logo */ +.logo-back{ + position:absolute;left:0;right:0;top:50%;transform:translateY(-50%); + z-index:2;display:flex;flex-direction:column;align-items:center;gap:.18in;padding:0 .5in; +} +.logo-back img{width:2in;max-width:62%;height:auto;} +.logo-sm{position:relative;z-index:2;margin-top:.12in;} +.logo-sm img{width:1in;height:auto;} + +.grid4 .cell .logo-sm img{width:.85in;} + +/* flat 4-up grid */ +.grid4{position:absolute;inset:0;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;} +.grid4 .cell{position:relative;overflow:hidden;} +.grid4 .cell .face{padding:.4in .35in;} +.grid4 .cell .ttl{font-size:calc(var(--c-titleSize,40px) * .62);} +.grid4 .cell .sub{font-size:calc(var(--c-subSize,16px) * .9);} +.grid4 .cell .msg{font-size:calc(var(--c-msgSize,15px) * .92);margin-top:.12in;} + +/* quarter-fold: 4 panels on a portrait sheet, top row prints upside-down */ +.qgrid{position:absolute;inset:0;display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;} +.qcell{position:relative;overflow:hidden;} +.qcell.rot .face{transform:rotate(180deg);} + +.qgrid .ttl{font-size:calc(var(--c-titleSize,40px) * .6);} +.qgrid .sub{font-size:calc(var(--c-subSize,16px) * .88);} +.qgrid .msg{font-size:calc(var(--c-msgSize,15px) * .95);} +.qgrid .face{padding:.4in .35in;} +.qgrid .face.inside-r{padding-top:.55in;} +.qgrid .logo-back img{width:1.4in;max-width:70%;} + +/* screen-only quadrant labels */ +.qlabels{position:absolute;inset:0;z-index:7;display:grid; +grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;pointer-events:none;} +.qlabels span{align-self:start;justify-self:center;margin-top:8px; +font-family:var(--chrome-font);font-size:10px;font-weight:600;letter-spacing:.03em; +background:rgba(20,22,28,.72);color:#fff;padding:3px 8px;border-radius:999px;} + +/* ---------- Mobile overrides ---------- */ +@media (max-width:960px){ + header.topbar{ padding:12px 16px; } + .brand h1{ font-size:16px; } + .brand .tag{ display:none; } + + /* Layout fills exactly the space below the header — body never scrolls */ + .layout{ + flex-direction:column; + height:calc(100vh - 49px); + overflow:hidden; + } + + /* Preview: fixed slice at the top of the layout column */ + .previewOuter{ + order:-1; + position:static; /* no sticky needed — body doesn't scroll */ + flex:none; + height:46vh; + min-height:240px; + width:100%; + } + .previewWrap{ + width:100%;height:100%; + overflow-y:auto;overflow-x:hidden; + padding:20px 10px; + gap:16px; + border-bottom:1px solid var(--line); + background:var(--bench); + box-shadow:0 4px 12px rgba(0,0,0,.06); + } + + /* Controls: take the remaining height and scroll internally */ + .controls{ + width:100%;flex:1;min-height:0; + overflow-y:auto;overflow-x:hidden; + border-right:0;padding:0 0 20px; + height:auto; + } + .tab-panel{ padding:14px 16px 0; } + .swatches{ grid-template-columns:repeat(3,1fr); } + + .btn{ padding:12px 18px;font-size:14px; } + .seg button{ padding:12px 6px;font-size:14px; } + .preset{ padding:10px 15px; } + .field input,.field textarea{ padding:14px 12px;font-size:16px; } + .opt{ font-size:14px; } + .opt input{ width:18px;height:18px; } + .group > .lbl{ font-size:12px;margin-bottom:12px; } + .group{ margin-bottom:28px; } + .sheetLabel{ font-size:13px;top:-30px; } +} + +@media (max-width:380px){ + .swatches{grid-template-columns:repeat(2,1fr);} +} + +/* ---------- Print ---------- */ +@media print{ + .no-print{display:none !important;} + html,body{background:#fff !important; max-width: none !important; overflow: visible !important;} + .layout{display:block; width: auto !important; max-width: none !important; overflow: visible !important;} + .previewOuter{height:auto !important;} + .previewWrap{padding:0;margin:0;height:auto;overflow:visible !important;background:#fff !important;gap:0;display:block;} + .sheetHolder{width:auto !important;height:auto !important;margin:0;} + .sheetLabel{display:none;} + .sheet{ + position:relative !important; + transform:none !important;box-shadow:none !important;margin:0 !important; + page-break-after:auto;break-after:auto; + } + .guide.noprint-guide{display:none !important;} + .guide .gtxt{display:none !important;} +} +