Initial commit — Card Studio printable card generator

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 <noreply@anthropic.com>
This commit is contained in:
chris 2026-06-16 21:33:49 -04:00
commit fb03a99f2c
7 changed files with 1319 additions and 0 deletions

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.503384" d="M212.378 105.311C205.388 92.3073 200.476 72.4068 210.655 60.0771C221.984 46.3554 227.413 74.5169 227.891 82.6941C228.3 89.6879 227.342 97.6138 225.019 106.472C246.425 66.5484 261.553 50.3097 270.405 57.7556C285.578 70.52 234.787 111.693 238.232 111.693C252.26 111.693 272.546 77.4567 289.364 100.094C290.766 101.98 245.068 127.138 238.805 125.032C238.73 125.004 271.396 116.432 278.446 137.789C285.001 157.633 239.789 130.832 241.105 130.832C247.988 130.832 251.319 172.587 234.21 172.587C223.027 172.587 221.628 136.931 228.464 131.41C235.071 126.077 213.263 178.3 196.292 172.587C186.239 169.205 197.331 130.796 204.337 133.149C208.656 134.605 195.023 138.993 191.123 136.632C185.927 133.485 183.104 129.671 185.951 118.071C188.798 106.472 210.548 98.6456 212.378 113.432" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.636254" d="M277.205 167.196C243.354 233.919 332.485 198.827 319.148 169.982C309.589 149.306 279.921 149.306 298.119 180.585" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M125.063 62.2249C143.849 152.865 180.581 246.415 175.656 341.093" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M209.721 181.74C189.208 232.8 195.232 291.733 182.332 343.582" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M266.989 214.108C223.835 244.27 213.25 301.629 187.312 344.827" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M182.331 343.582C171.549 282.499 146.469 236.182 121.329 195.434" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.476539" d="M120.414 149.579C122.739 162.122 121.654 183.699 104.806 186.409C71.7166 191.732 72.4839 126.711 95.5583 132.28C110.348 135.85 101.816 169.237 88.0444 154.042" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.645717" d="M161.029 82.1438C165.89 104.483 156.785 126.876 151.205 148.126" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.577218" d="M143.738 136.921C137.686 126.654 127.946 121.437 121.329 112.022" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path opacity="0.482143" d="M162.412 190.807C167.598 97.2188 195.367 174.786 163.594 195.434" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M197.723 221.382C200.973 217.829 204.726 214.916 208.316 211.729C275.359 152.163 205.795 267.154 194.78 221.987" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M189.8 237.708C179.851 211.113 166.39 240.097 186.596 242.742" stroke="#000000" stroke-opacity="0.9" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
width="800px" height="800px" viewBox="0 0 136.492 136.492"
xml:space="preserve">
<g>
<path d="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.292
c-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.493
c0.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.169
c-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.354
c0.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.576
c-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.572
c-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.563
c2.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.686
c0.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.427
c0.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.232
c-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,0
C21.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.822
c-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.093
c3.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.438
c-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.402
c-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.472
c0.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.563
c-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.633
c2.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.193
c-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.089
c1.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.087
c-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.443
c-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.332
c6.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.672
C21.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.22
c0.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.847
C39.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.289
c-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.653
L33.705,55.653z M47.734,47.466L28.589,27.061c1.859-0.864,3.889-1.653,6.088-2.339l17.65,17.216
C50.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.858
c-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.218
c-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.676
z 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.723
l-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.996
c2.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.406
c2.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.449
z 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.161
C37.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.992
c1.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"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

212
index.html Normal file
View File

@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Card Studio — Printable Cards</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;700&family=Cinzel:wght@400;700&family=Dancing+Script:wght@400;700&family=Fredoka:wght@400;600&family=Lora:ital,wght@0,400;0,700;1,400&family=Montserrat:wght@400;700;800&family=Pacifico&family=Playfair+Display:ital,wght@0,400;0,700;0,800;1,400&family=Quicksand:wght@400;600;700&family=Raleway:ital,wght@0,400;0,700;1,400&family=Press+Start+2P&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="topbar no-print">
<div class="brand">
<h1>Card Studio</h1>
<span class="tag">printable cards</span>
</div>
<button class="btn primary" id="printBtn">Print card</button>
</header>
<div class="layout">
<aside class="controls no-print">
<div class="tabs" role="tablist" id="tabBar">
<button class="tab active" data-tab="design" role="tab" aria-selected="true">Design</button>
<button class="tab" data-tab="text" role="tab" aria-selected="false">Text</button>
<button class="tab" data-tab="options" role="tab" aria-selected="false">Options</button>
</div>
<!-- ── Design ─────────────────────────────────────── -->
<div class="tab-panel active" data-tab="design">
<div class="group">
<span class="lbl">Paper size</span>
<div class="seg" id="paperSeg" role="group" aria-label="Paper size">
<button data-paper="letter" aria-pressed="true">Letter<br><span style="font-weight:400;font-size:11px;">8.5 × 11 in</span></button>
<button data-paper="a4" aria-pressed="false">A4<br><span style="font-weight:400;font-size:11px;">210 × 297 mm</span></button>
</div>
</div>
<div class="group">
<span class="lbl">Card style</span>
<div class="seg" id="styleSeg" role="group" aria-label="Card style">
<button data-style="quarter" aria-pressed="true">Quarter-fold<br>(small)</button>
<button data-style="flat" aria-pressed="false">Flat ×4</button>
</div>
<div class="note" id="styleNote" style="margin-top:10px;">
<b>Quarter-fold:</b> one portrait sheet, folded twice, makes a small card — front, back, and message all on one sheet.
</div>
</div>
<div class="group">
<span class="lbl">Theme</span>
<div class="swatches" id="swatches"></div>
</div>
<div class="group">
<span class="lbl">Typography</span>
<div class="field">
<label for="fFont">Font</label>
<select id="fFont" style="width:100%;font:inherit;font-size:14px;padding:8px;border-radius:8px;border:1px solid var(--line);background:#fcfbf8;">
<option value="">(Theme default)</option>
<optgroup label="Serif (Elegant)">
<option value="'Playfair Display', serif">Playfair Display</option>
<option value="'Lora', serif">Lora</option>
<option value="'Cinzel', serif">Cinzel (Classical)</option>
</optgroup>
<optgroup label="Sans-Serif (Modern)">
<option value="'Montserrat', sans-serif">Montserrat</option>
<option value="'Raleway', sans-serif">Raleway</option>
<option value="'Quicksand', sans-serif">Quicksand (Soft)</option>
</optgroup>
<optgroup label="Script &amp; Handwritten">
<option value="'Dancing Script', cursive">Dancing Script</option>
<option value="'Pacifico', cursive">Pacifico</option>
<option value="'Caveat', cursive">Caveat</option>
</optgroup>
<optgroup label="Playful">
<option value="'Fredoka', sans-serif">Fredoka</option>
</optgroup>
<optgroup label="Pixel &amp; Retro">
<option value="'Press Start 2P', monospace">Press Start 2P</option>
</optgroup>
</select>
</div>
<div class="field">
<label>Alignment</label>
<div class="seg" id="alignSeg" role="group" aria-label="Text alignment">
<button data-align="left">Left</button>
<button data-align="center" aria-pressed="true">Center</button>
<button data-align="right">Right</button>
</div>
</div>
<div class="field">
<label for="fTitleScale">Title size <span class="slider-val" id="fTitleScaleVal">100%</span></label>
<input type="range" id="fTitleScale" min="50" max="200" step="5" value="100">
</div>
<div class="field" style="margin-bottom:0">
<label for="fBodyScale">Body size <span class="slider-val" id="fBodyScaleVal">100%</span></label>
<input type="range" id="fBodyScale" min="50" max="200" step="5" value="100">
</div>
</div>
<div class="group" style="margin-bottom:0">
<span class="lbl">Colors</span>
<div class="colorrow">
<label class="ci">Accent <input type="color" id="cAccent"></label>
<label class="ci">Text <input type="color" id="cText"></label>
<label class="ci">Background <input type="color" id="cBg"></label>
<button class="reset-link" id="resetColors" type="button">Reset to theme</button>
</div>
</div>
</div><!-- /design -->
<!-- ── Text ───────────────────────────────────────── -->
<div class="tab-panel" data-tab="text">
<div class="group">
<span class="lbl">Quick start</span>
<div class="presets" id="presets"></div>
</div>
<div class="group" style="margin-bottom:0">
<span class="lbl">Text</span>
<div class="field">
<label for="fTitle">Front title</label>
<input id="fTitle" type="text" maxlength="60" value="Happy Birthday">
</div>
<div class="field">
<label for="fSub">Front subtitle</label>
<input id="fSub" type="text" maxlength="80" value="Wishing you a wonderful day">
</div>
<div class="field">
<label for="fMsg" id="msgLabel">Message</label>
<textarea id="fMsg" maxlength="400">Hope your day is filled with joy, laughter, and a little extra cake.
With love,</textarea>
<div class="hint" id="msgHint">Printed inside the quarter-fold card, or on each flat card.</div>
</div>
<div class="field" id="msgSideWrap">
<label>Message page</label>
<div class="seg" id="msgSideSeg" role="group" aria-label="Message page">
<button data-msgside="left">Left page</button>
<button data-msgside="right" aria-pressed="true">Right page</button>
</div>
</div>
<div class="field" style="margin-bottom:0">
<label for="fBack">Back footer <span style="color:var(--muted);font-weight:400;">(optional)</span></label>
<input id="fBack" type="text" maxlength="50" value="">
</div>
</div>
</div><!-- /text -->
<!-- ── Options ────────────────────────────────────── -->
<div class="tab-panel" data-tab="options">
<div class="group">
<span class="lbl">Branding</span>
<label class="opt">
<input type="checkbox" id="cLogo" checked>
<span>Include the Beach Party Balloons logo
<span class="sub">Loads from beachpartyballoons.com — needs internet.</span>
</span>
</label>
<div id="logoPosWrap">
<span class="lbl" style="margin-bottom:6px;">Logo position</span>
<div class="seg" id="logoSeg" role="group" aria-label="Logo position">
<button data-pos="back" aria-pressed="true">Back</button>
<button data-pos="front" aria-pressed="false">Front</button>
</div>
</div>
</div>
<div class="group">
<span class="lbl">Print guides</span>
<label class="opt">
<input type="checkbox" id="cGuide" checked>
<span id="guideOptLabel">Show fold lines when printing
<span class="sub" id="guideOptSub">A faint dashed crease to fold against.</span>
</span>
</label>
</div>
<div class="note" style="margin-bottom:0">
<b>Printing tips.</b> In the print dialog set margins to <b>None</b> and enable <b>Background graphics</b> so colors show. Match the paper size in your print dialog to the Design tab selection.
</div>
<!-- kept in DOM for JS compatibility, never visible -->
<div id="optGroup" style="display:none"><input type="checkbox" id="cInside"></div>
<div id="printGroup" style="display:none">
<input type="checkbox" id="cDouble">
<div id="sideSelector"><div class="seg" id="sideSeg"><button data-side="outside" aria-pressed="true">Outside</button><button data-side="inside" aria-pressed="false">Inside</button></div></div>
</div>
</div><!-- /options -->
</aside>
<div class="previewOuter">
<div class="previewWrap" id="previewWrap">
<div id="sheets"></div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>

640
script.js Normal file
View File

@ -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+=`<rect x="${p[0]}" y="${p[1]}" width="2.4" height="2.4" rx=".5" transform="rotate(${i*25} ${p[0]} ${p[1]})" fill="${col}"/>`;
else if(i%3===1) s+=`<circle cx="${p[0]}" cy="${p[1]}" r="1.4" fill="${col}"/>`;
else s+=`<path d="M${p[0]} ${p[1]} l2 3 l-4 0 z" fill="${col}"/>`;
});
return svg(s);
}
case 'gold':{
const a=theme.vars['--c-accent'];
const fr=`<rect x="4" y="6" width="92" height="88" fill="none" stroke="${a}" stroke-width=".5"/>
<rect x="6" y="9" width="88" height="82" fill="none" stroke="${a}" stroke-width="1.1"/>`;
const fl=(x,y,r)=>`<g transform="translate(${x} ${y}) rotate(${r})" fill="none" stroke="${a}" stroke-width=".7">
<path d="M0 0 q7 0 9 6 q-6 -1 -9 -6"/><path d="M0 0 q0 7 6 9 q-1 -6 -6 -9"/><circle cx="0" cy="0" r=".9" fill="${a}"/></g>`;
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)=>`<g transform="translate(${tx},${ty}) rotate(${r}) scale(${s})" fill="${a}" fill-opacity="${op}"><path d="${vp}"/></g>`;
// 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) => `<ellipse cx="${x}" cy="${y}" rx="${rx}" ry="${ry}" transform="rotate(${r} ${x} ${y})" fill="${col}" fill-opacity="${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)=>`<g transform="translate(${x} ${y}) rotate(${r}) scale(${sc})">
<path d="M0 12 Q 2 25 -2 35" stroke="${col}" stroke-width=".4" fill="none" opacity=".6"/>
<ellipse cx="0" cy="0" rx="7" ry="9" fill="${col}"/>
<path d="M0 9 l-1.5 2 h3 z" fill="${col}"/>
<ellipse cx="-2.5" cy="-3.5" rx="1.8" ry="2.5" fill="#fff" fill-opacity=".3" transform="rotate(-20 -2.5 -3.5)"/>
</g>`;
// Group of balloons tied together
const group=`<g transform="translate(50 65)">
${bal(-8,-12,cols[0],1.2,10)}${bal(8,-10,cols[1],1.1,-10)}${bal(0,-20,cols[2],1.3,0)}
<path d="M-5 18 Q 0 25 0 35 M 5 18 Q 0 25 0 35 M 0 10 Q 0 25 0 35" stroke="#ccc" stroke-width=".3" fill="none"/>
</g>`;
// Floating sparkles
let sparkles='';
for(let i=0;i<12;i++) sparkles+=`<circle cx="${Math.random()*100}" cy="${Math.random()*100}" r="${0.4+Math.random()*0.8}" fill="${cols[i%3]}" fill-opacity=".4"/>`;
return svg(sparkles + group);
}
case 'kraft':{
const a=theme.vars['--c-accent'];
const border=`<rect x="5" y="6" width="90" height="88" fill="none" stroke="${a}" stroke-width=".8" stroke-dasharray="2 1.6"/>`;
const sprig=`<g transform="translate(50 16)" stroke="${a}" stroke-width=".9" fill="none" stroke-linecap="round">
<path d="M-9 0 H9"/><path d="M-9 0 q-2 -3 -5 -3"/><path d="M9 0 q2 -3 5 -3"/><circle cx="0" cy="0" r="1.4" fill="${a}"/></g>`;
return svg(border+sprig);
}
case 'flowers':{
const a=theme.vars['--c-accent'];
const img=(href,x,y,w,h,op=1)=>`<image href="${href}" x="${x}" y="${y}" width="${w}" height="${h}" opacity="${op}"/>`;
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+=`<circle cx="${x}" cy="${y}" r="1.8" fill="${a}" opacity=".18"/>`;
});
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+=`<rect x="${x}" y="${y}" width="${i%4===0?1.5:1}" height="${i%4===0?1.5:1}" fill="${col}" opacity=".6"/>`;
});
// Synthwave horizon sun
const hz=70;
s+=`<ellipse cx="50" cy="${hz}" rx="16" ry="8" fill="${a}" opacity=".2"/>`;
s+=`<ellipse cx="50" cy="${hz}" rx="11" ry="5.5" fill="${a}" opacity=".28"/>`;
[0,2.4,4.8].forEach(dy=>{ s+=`<rect x="34" y="${hz-1.2+dy}" width="32" height=".9" fill="#0D0A1E" opacity=".85"/>`; });
// Perspective grid lines
for(let i=0;i<=6;i++)
s+=`<line x1="50" y1="${hz}" x2="${(i/6)*100}" y2="100" stroke="${a}" stroke-width=".4" opacity=".4"/>`;
for(let i=1;i<=4;i++){
const t=i/4, y=hz+t*(100-hz), hw=t*50;
s+=`<line x1="${50-hw}" y1="${y}" x2="${50+hw}" y2="${y}" stroke="${a}" stroke-width=".4" opacity=".4"/>`;
}
// 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+=`<rect x="${ox+c*ps}" y="${oy+r*ps}" width="${ps-.2}" height="${ps-.2}" fill="${cy}"/>`;
}));
// Neon corner brackets
const BW=6, BT=1.2;
s+=`<rect x="3" y="3" width="${BW}" height="${BT}" fill="${cy}"/>`;
s+=`<rect x="3" y="3" width="${BT}" height="${BW}" fill="${cy}"/>`;
s+=`<rect x="${97-BW}" y="3" width="${BW}" height="${BT}" fill="${cy}"/>`;
s+=`<rect x="${97-BT}" y="3" width="${BT}" height="${BW}" fill="${cy}"/>`;
s+=`<rect x="3" y="${97-BT}" width="${BW}" height="${BT}" fill="${cy}"/>`;
s+=`<rect x="3" y="${97-BW}" width="${BT}" height="${BW}" fill="${cy}"/>`;
s+=`<rect x="${97-BW}" y="${97-BT}" width="${BW}" height="${BT}" fill="${cy}"/>`;
s+=`<rect x="${97-BT}" y="${97-BW}" width="${BT}" height="${BW}" fill="${cy}"/>`;
return svg(s);
}
}
return svg('');
}
function svg(inner){
return `<svg viewBox="0 0 100 100" preserveAspectRatio="none" width="100%" height="100%" style="position:absolute;inset:0">${inner}</svg>`;
}
/* ---- 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 youve accomplished. Heres to what comes next!", font:"Montserrat, sans-serif" },
{ name:"Invitation", title:"Youre 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 youre 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 youve accomplished. This is just the beginning of an incredible journey!\n\nWith love,", font:"Montserrat, sans-serif" },
{ name:"Valentines", title:"Happy Valentines 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:'<b>Quarter-fold:</b> 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:'<b>Flat:</b> 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=`<span class="chip" style="background:${t.chipBg}"><span style="color:${t.chipInk}">${t.chipGlyph}</span></span><span class="nm">${t.name}</span>`;
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:'<b>Quarter-fold:</b> 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:'<b>Flat:</b> 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function logoImg(cls){
return `<div class="${cls}"><img src="${LOGO}" alt="Beach Party Balloons" onerror="this.parentNode.style.display='none'"></div>`;
}
function coverFace(){
const front = state.logo && state.logoPos==='front';
return `<div class="face" style="${faceVars()}">
<div class="deco">${deco(state.theme,'cover')}</div>
<div class="content">
${state.title?`<h2 class="ttl">${esc(state.title)}</h2>`:''}
${state.sub?`<p class="sub">${esc(state.sub)}</p>`:''}
${front?logoImg('logo-sm'):''}
</div>
</div>`;
}
function backFace(){
const back = state.logo && state.logoPos==='back';
return `<div class="face" style="${faceVars()};background:var(--c-bg)">
<div class="deco" style="opacity:.5">${deco(state.theme,'back')}</div>
${back?`<div class="logo-back">${logoImg('')}${state.back?`<div class="backmark" style="position:static">${esc(state.back)}</div>`:''}</div>`
:(state.back?`<div class="backmark">${esc(state.back)}</div>`:'')}
</div>`;
}
function insideFace(side){
const hasMsg = (side === (state.msgSide || 'right'));
if(hasMsg){
return `<div class="face inside-r" style="${faceVars()}">
<div class="content"><p class="msg">${esc(state.msg)}</p></div>
</div>`;
}
return `<div class="face inside-l" style="${faceVars()}"></div>`;
}
function flatCell(){
return `<div class="cell"><div class="face" style="${faceVars()}">
<div class="deco">${deco(state.theme,'cover')}</div>
<div class="content">
${state.title?`<h2 class="ttl">${esc(state.title)}</h2>`:''}
${state.sub?`<p class="sub">${esc(state.sub)}</p>`:''}
${state.msg?`<p class="msg">${esc(state.msg)}</p>`:''}
${state.logo?logoImg('logo-sm'):''}
</div>
</div></div>`;
}
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 = `<div class="sheet folded">
<div class="panel left">${backFace()}</div>
<div class="panel right">${coverFace()}</div>
<div class="${guideClass()} v" style="left:50%"><span class="gtxt">fold</span></div>
</div>`;
html += `<div class="sheetHolder ${manual && state.activeSide==='outside' ? 'active-print' : ''}">${holderLabel(state.inside?'Outside (front &amp; back)':'Card sheet — fold in half', outside)}</div>`;
}
if(state.inside && isInside){
const inside = `<div class="sheet folded second-sheet ${state.inside ? '' : 'no-double-sided'}">
<div class="panel left">${insideFace('left')}</div>
<div class="panel right">${insideFace('right')}</div>
<div class="${guideClass()} v" style="left:50%"><span class="gtxt">fold</span></div>
</div>`;
html += `<div class="sheetHolder ${manual && state.activeSide==='inside' ? 'active-print' : ''}">${holderLabel('Inside (message)', inside)}</div>`;
}
} else if(state.style==='quarter'){
const g = `<div class="qgrid">
<div class="qcell rot">${insideFace('left')}</div>
<div class="qcell rot">${insideFace('right')}</div>
<div class="qcell">${backFace()}</div>
<div class="qcell">${coverFace()}</div>
</div>`;
const labels = `<div class="qlabels no-print">
<span>Inside (prints upside-down)</span><span>Message </span>
<span>Back</span><span>Front cover</span>
</div>`;
const sheet = `<div class="sheet quarter">
${g}
<div class="${guideClass()} v" style="left:50%"></div>
<div class="${guideClass()} h" style="top:50%"></div>
${labels}
</div>`;
html += `<div class="sheetHolder active-print">${holderLabel('Small card — fold in half, then half again', sheet)}</div>`;
} else {
const flat = `<div class="sheet flat">
<div class="grid4">${flatCell()}${flatCell()}${flatCell()}${flatCell()}</div>
<div class="${guideClass()} v" style="left:50%"></div>
<div class="${guideClass()} h" style="top:50%"></div>
</div>`;
html += `<div class="sheetHolder active-print">${holderLabel('4 cards — cut to separate', flat)}</div>`;
}
sheets.innerHTML = html;
updatePageStyle();
fit();
saveState();
}
function holderLabel(label, inner){
return `<div class="sheetLabel">${label}</div>${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);

362
style.css Normal file
View File

@ -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;}
}