Diff -- /var/www/vhosts/webstls.com/httpdocs/TEMPLATES/webstls_storefront.html
Diff

/var/www/vhosts/webstls.com/httpdocs/TEMPLATES/webstls_storefront.html

added on WebSTLs (webstls.com) at 2026-07-01 22:27:05

Added
+1769
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to a9589e7bcd2a
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!-- =====================================================================
2 Storefront editor — populated from real DB data via storefront.cgi.
3 For an empty storefront the create form is shown by a separate
4 template (webstls_storefront_create.html).
5===================================================================== -->
6
7<div class="page-head">
8 <div>
9 <span class="page-eyebrow"><span class="dot"></span> Storefront · $store_status</span>
10 <h1 class="page-title">$store_name</h1>
11 <p class="page-subtitle">
12 <code>$store_url</code> &middot; Theme: $theme_label
13 </p>
14 </div>
15 <div class="page-actions">
16 <!-- Opens the real subdomain URL (3dshawn.webstls.com). Auth cookies
17 are host-only so they don't cross to subdomains, meaning the
18 owner naturally sees the same clean view a buyer would, AND
19 track.js fires under the right host so any clicks here populate
20 the seller's own heatmaps. -->
21 <a href="https://$store_url" target="_blank" rel="noopener" class="btn btn-secondary" title="Opens your real public storefront URL in a new tab -- you see exactly what a visitor sees, and your clicks populate your heatmaps.">
22 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
23 View as buyer
24 </a>
25 [if:$has_custom_domain]
26 <a href="https://$store_custom" target="_blank" rel="noopener" class="btn btn-secondary">
27 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
28 Visit live
29 </a>
30 [/if]
31 </div>
32</div>
33
34<!-- ===== Open / Close storefront ===================================
35 Lets the seller flip their storefront between live (buyers can
36 shop) and closed (maintenance page shown). Status pill on the
37 left shows current state; explanations under each button make
38 it clear what the action does so they aren't afraid of either
39 button. Suspended / draft states render a read-only notice
40 instead of the toggle so the seller can't accidentally exit
41 an admin-imposed state from here.
42
43 Rendered ABOVE the share-link callout because status is the
44 thing the seller wants to see first when they land on this
45 page -- "is my store live or closed right now?" -- before any
46 of the URL-sharing housekeeping.
47================================================================== -->
48[if:$is_store_live]
49<div style="display:flex;gap:18px;align-items:stretch;background:linear-gradient(135deg,rgba(16,185,129,0.10),rgba(16,185,129,0.04));border:1px solid rgba(16,185,129,0.40);border-radius:14px;padding:18px 22px;margin:0 0 20px;flex-wrap:wrap">
50 <div style="flex:1;min-width:240px">
51 <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
52 <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#10b981;box-shadow:0 0 8px rgba(16,185,129,0.7)"></span>
53 <span style="font-size:11px;letter-spacing:2px;text-transform:uppercase;font-weight:700;color:#6ee7b7">Storefront is OPEN</span>
54 </div>
55 <div style="font-size:14px;color:var(--col-text);font-weight:600;margin-bottom:4px">Buyers can browse and purchase right now.</div>
56 <div style="font-size:12.5px;color:var(--col-text-2);line-height:1.55">Closing the storefront swaps the public URL for a short "we'll be right back" maintenance page &mdash; your URL stays the same, just nobody can buy until you re-open. Use it for breaks, big inventory edits, or anything you want buyers to wait through.</div>
57 </div>
58 <form method="POST" action="/storefront.cgi" style="margin:0;display:flex;align-items:center">
59 <input type="hidden" name="action" value="toggle_status">
60 <input type="hidden" name="store_id" value="$store_id">
61 <input type="hidden" name="target" value="closed">
62 <button type="submit" class="btn btn-secondary" style="display:inline-flex;align-items:center;gap:8px;padding:11px 18px;border-color:rgba(239,68,68,0.50);color:#fca5a5"
63 data-confirm-title="Close your storefront?"
64 data-confirm-body="Buyers will see a friendly maintenance page until you re-open. Your data, models, settings, and orders are not touched."
65 data-confirm-ok="Yes, close storefront"
66 data-confirm-tone="danger">
67 <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
68 Close storefront
69 </button>
70 </form>
71</div>
72[/if]
73[if:$is_store_closed]
74<div style="display:flex;gap:18px;align-items:stretch;background:linear-gradient(135deg,rgba(239,68,68,0.10),rgba(239,68,68,0.04));border:1px solid rgba(239,68,68,0.45);border-radius:14px;padding:18px 22px;margin:0 0 20px;flex-wrap:wrap">
75 <div style="flex:1;min-width:240px">
76 <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
77 <span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,0.7)"></span>
78 <span style="font-size:11px;letter-spacing:2px;text-transform:uppercase;font-weight:700;color:#fca5a5">Storefront is CLOSED</span>
79 </div>
80 <div style="font-size:14px;color:var(--col-text);font-weight:600;margin-bottom:4px">Buyers see a "we'll be right back" maintenance page.</div>
81 <div style="font-size:12.5px;color:var(--col-text-2);line-height:1.55">Your storefront is paused. Anyone landing on your URL sees a friendly maintenance notice instead of the catalog. All your models, settings, and orders stay exactly as they were &mdash; opening it again puts buyers right back in.</div>
82 </div>
83 <form method="POST" action="/storefront.cgi" style="margin:0;display:flex;align-items:center">
84 <input type="hidden" name="action" value="toggle_status">
85 <input type="hidden" name="store_id" value="$store_id">
86 <input type="hidden" name="target" value="live">
87 <button type="submit" class="btn btn-primary" style="display:inline-flex;align-items:center;gap:8px;padding:11px 18px;box-shadow:0 6px 18px rgba(16,185,129,0.40);background:linear-gradient(135deg,#10b981,#059669);border:none">
88 <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>
89 Open storefront
90 </button>
91 </form>
92</div>
93[/if]
94[if:$is_store_other]
95<div style="background:rgba(245,158,11,0.10);border:1px solid rgba(245,158,11,0.40);border-radius:14px;padding:14px 18px;margin:0 0 20px;font-size:13px;color:#fde68a">
96 Storefront status: <strong>$store_status</strong>. Only WebSTLs admins can change this state. <a href="mailto:support\@webstls.com" style="color:#fbbf24">Contact support</a> if you have questions.
97</div>
98[/if]
99
100<!-- ===== Share / view-as-buyer callout ===============================
101 Most new sellers don't realise the URL they're already using IS
102 the public storefront URL. This callout makes the two viewing
103 modes explicit:
104 - PUBLIC LINK to copy / share with buyers + friends
105 - VIEW AS BUYER opens the storefront without the owner banner
106 Combined, no more confusion about "which URL do I send to people".
107===================================================================== -->
108<div style="background:linear-gradient(135deg,rgba(124,58,237,0.14),rgba(37,99,235,0.10),rgba(6,182,212,0.08));border:1px solid rgba(124,58,237,0.30);border-radius:14px;padding:18px 20px;margin:0 0 20px">
109 <div style="font-size:11px;letter-spacing:0.14em;text-transform:uppercase;color:#c4b5fd;font-weight:700">Your public storefront link</div>
110 <div style="display:flex;align-items:center;gap:10px;margin-top:8px;flex-wrap:wrap">
111 <code id="storefront-share-url" style="font-size:18px;color:#fff;background:rgba(15,23,42,0.55);padding:8px 12px;border-radius:8px;border:1px solid var(--col-border);word-break:break-all;font-family:var(--font-mono,monospace)">https://$store_url</code>
112 <button type="button" data-copy-target="#storefront-share-url" class="btn btn-secondary btn-sm" style="flex:0 0 auto">
113 <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
114 <span data-copy-label>Copy link</span>
115 </button>
116 <!-- View as buyer hits the real subdomain URL. Cookies don't cross
117 subdomains so the seller is automatically rendered as a visitor
118 there -- AND track.js fires under the right host, so any clicks
119 the seller does here count toward THEIR heatmap data. -->
120 <a href="https://$store_url" target="_blank" rel="noopener" class="btn btn-secondary btn-sm" style="white-space:nowrap;flex:0 0 auto" title="Opens the live storefront URL in a new tab so you see exactly what a buyer sees.">
121 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
122 View as buyer
123 </a>
124 <!-- Open in edit mode goes through the main domain where the auth
125 cookie applies, so the seller sees their owner Preview banner
126 with the Edit link. -->
127 <a href="/store.cgi?id=$store_id" target="_blank" class="btn btn-secondary btn-sm" style="white-space:nowrap;flex:0 0 auto" title="The version YOU see while logged in &mdash; with the Preview-mode banner and edit links.">
128 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
129 Open in edit mode
130 </a>
131 </div>
132 <div style="font-size:12.5px;color:var(--col-text-2);margin-top:10px;line-height:1.55">
133 Share this with buyers, post it on social, send it to friends &mdash; it's your real public storefront URL.
134 <strong style="color:#fff">Visiting it yourself shows the exact same clean view buyers see</strong> (no editing chrome),
135 and any clicks you make there fold into your own Heatmaps. To edit the page instead,
136 use <strong style="color:#fff">Open in edit mode</strong> &rarr; which loads the editor view with the Preview banner + edit links.
137 </div>
138</div>
139
140<!-- Tiny script to copy the share URL to the clipboard when the seller
141 hits the Copy link button. Falls back to selecting the text if the
142 async clipboard API is blocked (older browsers / strict CSP). -->
143<script>
144(function () {
145 var btn = document.querySelector('[data-copy-target="#storefront-share-url"]');
146 if (!btn) return;
147 var lbl = btn.querySelector('[data-copy-label]');
148 btn.addEventListener('click', function () {
149 var el = document.querySelector(btn.getAttribute('data-copy-target'));
150 if (!el) return;
151 var txt = el.textContent || '';
152 var done = function () {
153 if (lbl) lbl.textContent = 'Copied!';
154 setTimeout(function () { if (lbl) lbl.textContent = 'Copy link'; }, 1600);
155 };
156 if (navigator.clipboard && navigator.clipboard.writeText) {
157 navigator.clipboard.writeText(txt).then(done).catch(function () {
158 var r = document.createRange(); r.selectNode(el);
159 var s = window.getSelection(); s.removeAllRanges(); s.addRange(r);
160 try { document.execCommand('copy'); done(); } catch (e) { }
161 });
162 } else {
163 var r = document.createRange(); r.selectNode(el);
164 var s = window.getSelection(); s.removeAllRanges(); s.addRange(r);
165 try { document.execCommand('copy'); done(); } catch (e) { }
166 }
167 });
168})();
169</script>
170
171
172<!-- ===== Tabs + compact stats inline ================================
173 The tabs row used to sit alone with a big 3-card stat grid
174 above it. Compact-mode collapses the stats into a tight pill
175 row that floats to the right edge of the tabs section, so the
176 three operational numbers (products, orders today, revenue
177 today) are always glanceable without taking a full row. -->
178<style>
179 .sf-tabrow { display:flex; align-items:center; gap:18px; flex-wrap:wrap; margin-bottom: var(--space-lg); }
180 /* The shared .tabs class carries margin-bottom for stand-alone tab
181 bars; inside this row it would push the tabs visually up relative
182 to the stat pills (since align-items:center centers margin-boxes).
183 Strip it here and lift the spacing to the row container. */
184 .sf-tabrow .tabs { flex:1; min-width:0; margin-bottom:0; }
185 .sf-mini-stats {
186 display:flex; gap:10px; flex-wrap:nowrap;
187 margin-left:auto;
188 }
189 .sf-mini-stat {
190 background: var(--col-surface-2);
191 border: 1px solid var(--col-border);
192 border-radius: 10px;
193 padding: 8px 14px;
194 min-width: 92px;
195 display: flex; flex-direction: column;
196 line-height: 1.15;
197 }
198 .sf-mini-stat .lbl {
199 font-size: 9px; font-weight: 700;
200 letter-spacing: 1.5px; text-transform: uppercase;
201 color: var(--col-text-3);
202 white-space: nowrap;
203 }
204 .sf-mini-stat .val {
205 font-family: var(--font-display, inherit);
206 font-size: 18px; font-weight: 800;
207 color: var(--col-text);
208 margin-top: 2px;
209 }
210 @media (max-width: 900px) {
211 .sf-mini-stats { width:100%; margin-left:0; }
212 .sf-mini-stat { flex:1; }
213 }
214</style>
215<div class="sf-tabrow">
216 <div class="tabs" data-tab-group>
217 <div class="tab active" data-target="pages">Pages</div>
218 <div class="tab" data-target="design">Design</div>
219 <div class="tab" data-target="tests">Tests[if:$has_tests] &middot; $test_count[/if]</div>
220 <div class="tab" data-target="domain">Domain &amp; SSL</div>
221 <div class="tab" data-target="features">Features</div>
222 <div class="tab" data-target="payouts">Payouts</div>
223 </div>
224 <div class="sf-mini-stats">
225 <div class="sf-mini-stat" title="Listed products visible on this store">
226 <span class="lbl">Products</span>
227 <span class="val">$listing_count</span>
228 </div>
229 <div class="sf-mini-stat" title="Paid orders today">
230 <span class="lbl">Orders</span>
231 <span class="val">$orders_today_n</span>
232 </div>
233 <div class="sf-mini-stat" title="Gross revenue today (before fees)">
234 <span class="lbl">Revenue</span>
235 <span class="val">$revenue_today</span>
236 </div>
237 </div>
238</div>
239
240
241<!-- ===== Pages ====================================================== -->
242<div data-pane="pages">
243 <div class="module">
244 <div class="module-head">
245 <div class="left">
246 <div class="module-icon">
247 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
248 </div>
249 <div>
250 <div class="module-title">Pages</div>
251 <div class="module-sub">Edit each page's content. Preview anytime — drafts only show in preview, not on the live store.</div>
252 </div>
253 </div>
254 <a href="/page.cgi?store=$store_id&new=1" class="btn btn-secondary btn-sm">
255 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
256 New page
257 </a>
258 </div>
259
260 <div class="table-wrap" style="border:none;border-radius:0">
261 <table class="table">
262 <thead>
263 <tr>
264 <th>Page</th>
265 <th>Type</th>
266 <th>Menu link</th>
267 <th>Status</th>
268 <th class="num">Updated</th>
269 <th style="text-align:right">Actions</th>
270 </tr>
271 </thead>
272 <tbody>
273 [loop:@pages]
274 <tr>
275 <td>
276 <strong>$loop1.title</strong>
277 <div class="text-xs text-dim">$loop1.slug_display</div>
278 </td>
279 <td><span class="text-secondary">$loop1.page_type</span></td>
280 <td>
281 [if:$loop1.has_custom_menu_label]<span class="text-secondary">$loop1.menu_label_display</span>[/if]
282 [if:!$loop1.has_custom_menu_label]<span class="text-dim">$loop1.menu_label_display</span>[/if]
283 </td>
284 <td>$loop1.status_pill</td>
285 <td class="num text-dim">$loop1.updated_rel</td>
286 <td style="text-align:right">
287 [if:$loop1.menu_toggle_show]
288 <form method="POST" action="/storefront.cgi" class="pg-menu-form" data-page-id="$loop1.id" style="display:inline;margin:0" title="Toggle whether this page appears in the storefront's top navigation menu">
289 <input type="hidden" name="action" value="toggle_page_menu">
290 <input type="hidden" name="page_id" value="$loop1.id">
291 <input type="hidden" name="target" value="$loop1.in_menu_target" data-pg-target>
292 [if:$loop1.in_menu]
293 <button type="submit" class="btn btn-sm pg-menu-btn is-on" style="background:rgba(16,185,129,0.12);border:1px solid rgba(16,185,129,0.45);color:#6ee7b7;padding:6px 10px;vertical-align:middle" title="Currently shown in the storefront menu -- click to hide">
294 <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
295 <span data-pg-label>In menu</span>
296 </button>
297 [/if]
298 [if:!$loop1.in_menu]
299 <button type="submit" class="btn btn-sm pg-menu-btn is-off" style="background:transparent;border:1px solid var(--col-border);color:var(--col-text-3);padding:6px 10px;vertical-align:middle" title="Currently hidden from the storefront menu -- click to show">
300 <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
301 <span data-pg-label>Hidden</span>
302 </button>
303 [/if]
304 </form>
305 [/if]
306 [if:$loop1.is_deletable]
307 <form method="POST" action="/storefront.cgi" style="display:inline;margin:0"
308 data-confirm-title="Delete this page?"
309 data-confirm-body="The page and its content will be permanently removed. This cannot be undone."
310 data-confirm-ok="Delete page"
311 data-confirm-tone="danger">
312 <input type="hidden" name="action" value="delete_page">
313 <input type="hidden" name="page_id" value="$loop1.id">
314 <button type="submit" class="btn btn-sm" style="background:transparent;border:1px solid rgba(239,68,68,0.45);color:#f87171;padding:6px 10px;vertical-align:middle" title="Delete this page">
315 <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
316 </button>
317 </form>
318 [/if]
319 <a href="$loop1.view_url" target="_blank" class="btn btn-secondary btn-sm" title="Open this page in a new tab as a buyer would see it">
320 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
321 View
322 </a>
323 <a href="/page.cgi?id=$loop1.id" class="btn btn-secondary btn-sm" title="Edit page content">
324 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5Z"/></svg>
325 Edit
326 </a>
327 [if:$loop1.has_test]
328 <a href="#tests" class="btn btn-secondary btn-sm" data-tab-jump="tests" title="This page already has a $loop1.test_status_label A/B test -- click to manage it" style="background:rgba(124,58,237,0.18);border-color:rgba(124,58,237,0.55);color:#c4b5fd">
329 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
330 $loop1.test_status_label A/B
331 </a>
332 [/if]
333 [if:!$loop1.has_test]
334 <form method="POST" action="/storefront.cgi" style="display:inline;margin:0"
335 data-confirm-title="Start an A/B page test"
336 data-confirm-body="A draft copy of this page (slug-hidden so it doesn't appear in your nav) will be created and a draft A/B experiment wired to it. You'll be dropped into the variant page editor next so you can make your changes; then come back to the Tests tab and hit Start."
337 data-confirm-ok="Create variant + draft test"
338 data-confirm-tone="primary">
339 <input type="hidden" name="action" value="duplicate_page_for_test">
340 <input type="hidden" name="page_id" value="$loop1.id">
341 <button type="submit" class="btn btn-secondary btn-sm" title="Duplicate this page and create a draft A/B test" style="background:transparent;border:1px solid rgba(124,58,237,0.45);color:#c4b5fd">
342 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><path d="M12 2v20M2 12h20"/></svg>
343 A/B test
344 </button>
345 </form>
346 [/if]
347 </td>
348 </tr>
349 [/loop]
350 </tbody>
351 </table>
352 </div>
353
354 <div class="module-body" style="border-top:1px solid var(--col-border)">
355 <div class="banner info">
356 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
357 <div class="text-xs">
358 <strong>Preview without DNS setup.</strong> Your storefront is reachable right now at
359 <a href="/store.cgi?id=$store_id" target="_blank"><code>webstls.com/store.cgi?id=$store_id</code></a>
360 and at <a href="/store.cgi?subdomain=$store_subdomain" target="_blank"><code>?subdomain=$store_subdomain</code></a>.
361 When wildcard DNS is set up for <code>*.webstls.com</code>, it'll also be live at
362 <code>$store_subdomain.webstls.com</code>.
363 </div>
364 </div>
365 </div>
366 </div>
367</div>
368
369
370<!-- ===== Tests ====================================================== -->
371<div data-pane="tests" class="hide">
372 <div class="module">
373 <div class="module-head">
374 <div class="left">
375 <div class="module-icon" style="background:rgba(124,58,237,0.15);color:#c4b5fd">
376 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h6l3-9 4 18 3-9h4"/></svg>
377 </div>
378 <div>
379 <div class="module-title">A/B and MVT tests</div>
380 <div class="module-sub">Live experiments running against this storefront's pages and design. Click "A/B test" on any page in the Pages tab to spin up a new one.</div>
381 </div>
382 </div>
383 <a href="/optimization.cgi?new=1" class="btn btn-secondary btn-sm">Element / design test &rarr;</a>
384 </div>
385
386 [if:!$has_tests]
387 <div class="module-body">
388 <div class="banner info">
389 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
390 <div class="text-xs">
391 <strong>No experiments yet.</strong>
392 Start an A/B page test by clicking the <em>A/B test</em> button next to any page in the Pages tab,
393 or set up an element / design test in the <a href="/optimization.cgi?new=1" class="text-brand">Optimization page builder</a>.
394 </div>
395 </div>
396 </div>
397 [/if]
398
399 <!-- ---- New MVT page test form (block-level slots) -------------- -->
400 <div class="module-body" style="border-top:1px solid var(--col-border);background:rgba(124,58,237,0.04)">
401 <div style="display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap">
402 <div style="min-width:240px;flex:1">
403 <div style="font-weight:700;color:var(--col-text);margin-bottom:4px">New MVT block test</div>
404 <div class="text-xs text-dim" style="line-height:1.55">
405 Pick a page, name the slots you'll vary (comma-separated), and pick how many variants.
406 Then paste <code style="background:var(--col-surface-3);padding:2px 6px;border-radius:4px">{{slot:NAME}}</code> tokens into the page body via Edit; the storefront swaps them with the bucketed variant's value at render time.
407 </div>
408 </div>
409 <form method="POST" action="/storefront.cgi" style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end;min-width:520px">
410 <input type="hidden" name="action" value="create_mvt_page_test">
411 <div class="form-group" style="margin:0;min-width:160px">
412 <label class="form-label">Page</label>
413 <select class="select" name="mvt_page_id" required>
414 [loop:@pages]<option value="$loop1.id">$loop1.title ($loop1.slug_display)</option>[/loop]
415 </select>
416 </div>
417 <div class="form-group" style="margin:0;min-width:200px;flex:1">
418 <label class="form-label">Slot names</label>
419 <input class="input" type="text" name="mvt_slots" placeholder="hero, cta, about" required>
420 </div>
421 <div class="form-group" style="margin:0;width:110px">
422 <label class="form-label">Variants</label>
423 <select class="select" name="mvt_variants">
424 <option value="2">2 (A/B)</option>
425 <option value="3" selected>3</option>
426 <option value="4">4</option>
427 <option value="5">5</option>
428 <option value="6">6</option>
429 </select>
430 </div>
431 <button type="submit" class="btn btn-primary btn-sm">Create &rarr;</button>
432 </form>
433 </div>
434 </div>
435
436 [if:$has_tests]
437 <div class="table-wrap" style="border:none;border-radius:0">
438 <table class="table">
439 <thead>
440 <tr>
441 <th>Experiment</th>
442 <th>Type</th>
443 <th>Status</th>
444 <th class="num">Visitors</th>
445 <th>Variants</th>
446 <th style="text-align:right">Actions</th>
447 </tr>
448 </thead>
449 <tbody>
450 [loop:@test_rows]
451 <tr>
452 <td>
453 <a href="$loop1.detail_url" style="color:inherit;text-decoration:none">
454 <strong style="color:var(--col-accent-bright)">$loop1.name</strong>
455 </a>
456 [if:$loop1.is_page_test]
457 <div class="text-xs text-dim">Base page: <code>/$loop1.base_page_slug</code></div>
458 [/if]
459 [if:!$loop1.is_page_test]
460 <div class="text-xs text-dim">$loop1.surface_label</div>
461 [/if]
462 </td>
463 <td>$loop1.test_type</td>
464 <td>
465 [if:$loop1.is_draft]<span class="pill">Draft</span>[/if]
466 [if:$loop1.is_running]<span class="pill success">Running</span>[/if]
467 [if:$loop1.is_paused]<span class="pill warning">Paused</span>[/if]
468 [if:$loop1.is_complete]<span class="pill success">Complete</span>[/if]
469 [if:$loop1.is_aborted]<span class="pill">Archived</span>[/if]
470 </td>
471 <td class="num text-dim">$loop1.traffic_seen</td>
472 <td style="min-width:240px">
473 [loop:@loop1.variants]
474 <div style="display:flex;align-items:center;gap:6px;font-size:12px;margin-bottom:2px">
475 <span class="text-dim" style="display:inline-block;min-width:14px;text-align:center">$loop2.id</span>
476 [if:$loop2.has_page]
477 <a href="$loop2.edit_url" style="color:var(--col-text)">$loop2.page_title</a>
478 <a href="$loop2.view_url" target="_blank" class="text-dim" title="Preview this variant">view</a>
479 [/if]
480 [if:!$loop2.has_page]
481 <span style="color:var(--col-text-2)">$loop2.name</span>
482 [/if]
483 <span class="text-dim">&middot; $loop2.traffic_pct%</span>
484 </div>
485 [/loop]
486 </td>
487 <td style="text-align:right;white-space:nowrap">
488 [if:$loop1.is_draft]
489 <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0">
490 <input type="hidden" name="act" value="start">
491 <input type="hidden" name="id" value="$loop1.id">
492 <button type="submit" class="btn btn-success btn-sm">Start</button>
493 </form>
494 [/if]
495 [if:$loop1.is_running]
496 <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0">
497 <input type="hidden" name="act" value="pause">
498 <input type="hidden" name="id" value="$loop1.id">
499 <button type="submit" class="btn btn-warning btn-sm">Pause</button>
500 </form>
501 <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"
502 data-confirm-title="Stop and complete?"
503 data-confirm-body="Marks the test complete and locks in the leading variant. No more visitors are bucketed."
504 data-confirm-ok="Stop test"
505 data-confirm-tone="success">
506 <input type="hidden" name="act" value="complete">
507 <input type="hidden" name="id" value="$loop1.id">
508 <button type="submit" class="btn btn-success btn-sm">Stop</button>
509 </form>
510 [/if]
511 [if:$loop1.is_paused]
512 <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0">
513 <input type="hidden" name="act" value="start">
514 <input type="hidden" name="id" value="$loop1.id">
515 <button type="submit" class="btn btn-success btn-sm">Continue</button>
516 </form>
517 <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0"
518 data-confirm-title="Stop and complete?"
519 data-confirm-body="Marks the test complete and locks in the leading variant."
520 data-confirm-ok="Stop test"
521 data-confirm-tone="success">
522 <input type="hidden" name="act" value="complete">
523 <input type="hidden" name="id" value="$loop1.id">
524 <button type="submit" class="btn btn-success btn-sm">Stop</button>
525 </form>
526 [/if]
527 [if:$loop1.is_complete]
528 <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0">
529 <input type="hidden" name="act" value="start">
530 <input type="hidden" name="id" value="$loop1.id">
531 <button type="submit" class="btn btn-success btn-sm">Re-run</button>
532 </form>
533 [/if]
534 [if:$loop1.is_aborted]
535 <form method="POST" action="/optimization_action.cgi" style="display:inline;margin:0">
536 <input type="hidden" name="act" value="unarchive">
537 <input type="hidden" name="id" value="$loop1.id">
538 <button type="submit" class="btn btn-secondary btn-sm">Unarchive</button>
539 </form>
540 [/if]
541 [if:$loop1.is_block_test]
542 <a href="$loop1.edit_url" class="btn btn-secondary btn-sm">Edit slots</a>
543 [/if]
544 <a href="$loop1.detail_url" class="btn btn-secondary btn-sm">Report</a>
545 </td>
546 </tr>
547 [/loop]
548 </tbody>
549 </table>
550 </div>
551 [/if]
552 </div>
553</div>
554
555
556<!-- ===== Design (layout + color theme pickers) ====================== -->
557<div data-pane="design" class="hide" id="design">
558
559<style>
560 /* Picker thumbnails: slightly muted at rest so the active /
561 hovered card still pops as the focal point, but not so dim that
562 the colors and shapes are hard to read. The pre-2026 setting of
563 brightness(0.4) saturate(0.7) was too dark; cards looked black.
564 Hover lifts to full color as a clear "this is the one you're
565 about to pick" signal. */
566 .pic-thumb { filter: brightness(0.85) saturate(0.95); transition: filter 0.2s ease; }
567 .layout-card:hover .pic-thumb,
568 .theme-card:hover .pic-thumb,
569 .layout-card-active .pic-thumb,
570 .theme-card-active .pic-thumb { filter: brightness(1) saturate(1); }
571
572 /* Pagination controls under each picker grid */
573 .pic-pager { display:flex; gap:6px; justify-content:center; align-items:center; margin-top:24px; flex-wrap:wrap; }
574 .pic-pager button { background:var(--col-surface-1); border:1px solid var(--col-border); color:var(--col-text-2); padding:8px 14px; border-radius:8px; cursor:pointer; font-size:13px; font-weight:600; transition:all 0.15s; min-width:38px; }
575 .pic-pager button:hover:not(:disabled) { border-color:var(--col-accent); color:var(--col-text); }
576 .pic-pager button.active { background:var(--col-accent); color:#fff; border-color:var(--col-accent); }
577 .pic-pager button:disabled { opacity:0.4; cursor:not-allowed; }
578 .pic-pager .pic-info { font-size:12px; color:var(--col-text-3); margin:0 12px; font-family:var(--font-mono,monospace); letter-spacing:1px; text-transform:uppercase; }
579</style>
580
581<script>
582/* Picker pagination: only show ONE row of layouts / themes at a time so
583 the picker doesn't dominate the page and the user can find the
584 custom-themes section below. Page size is computed from the actual
585 grid width (CSS uses grid-template-columns:repeat(auto-fill,minmax(...)))
586 so "one row" stays accurate at any viewport. Recomputes on resize. */
587(function() {
588 function cardsPerRow(grid, cards) {
589 if (!cards.length) return 1;
590 // Fast path: .picker-grid-5 is a fixed-column-count grid whose
591 // breakpoints we control (5 / 3 / 2 / 1). Match them in JS so we
592 // never have to guess from runtime measurements -- prior attempts
593 // tripped over floating-point rounding, getComputedStyle returning
594 // the unresolved spec value, and offsetTop being 0 pre-layout.
595 if (grid.classList && grid.classList.contains('picker-grid-5')) {
596 var w = window.innerWidth;
597 if (w >= 850) return Math.min(5, cards.length);
598 if (w >= 650) return Math.min(3, cards.length);
599 if (w >= 450) return Math.min(2, cards.length);
600 return 1;
601 }
602 // For non-fixed grids (custom-themes uses auto-fill), measure.
603 // Read the resolved gridTemplateColumns; only trust it when the
604 // value is in resolved form (no parens / no "repeat(").
605 var tpl = (getComputedStyle(grid).gridTemplateColumns || '').trim();
606 if (tpl && tpl !== 'none' && tpl.indexOf('(') === -1) {
607 var parts = tpl.split(/\s+/).filter(function(s){ return s.length > 0; });
608 if (parts.length >= 1 && parts.length <= cards.length) return parts.length;
609 }
610 // Fallback A: offsetTop scan.
611 var firstTop = cards[0].offsetTop;
612 var n = 0;
613 for (var i = 0; i < cards.length; i++) {
614 if (cards[i].offsetTop === firstTop) n++;
615 else break;
616 }
617 if (n >= 1 && n < cards.length) return n;
618 // Fallback B: width arithmetic with Math.round.
619 var gridRect = grid.getBoundingClientRect();
620 var cardRect = cards[0].getBoundingClientRect();
621 var gap = parseFloat(getComputedStyle(grid).columnGap || getComputedStyle(grid).gap) || 18;
622 if (gridRect.width > 0 && cardRect.width > 0) {
623 var perRow = Math.round((gridRect.width + gap) / (cardRect.width + gap));
624 if (perRow >= 1 && perRow <= cards.length) return perRow;
625 }
626 return Math.min(5, cards.length);
627 }
628 function rowsSettingFor(grid) {
629 // How many rows to show. Source of truth is localStorage (so the
630 // user's choice survives reloads); the on-page <select> reflects
631 // and updates it. 0 = "All" (no pagination).
632 var key = grid.getAttribute('data-rows-key');
633 if (!key) return 1;
634 var raw = null;
635 try { raw = localStorage.getItem('ct_rows_' + key); } catch (e) {}
636 if (raw === null || raw === undefined) return 1;
637 var n = parseInt(raw, 10);
638 if (isNaN(n) || n < 0) return 1;
639 return n;
640 }
641 function paginate(grid) {
642 if (!grid) return;
643 // Cards are direct children of the grid. Built-in layout/theme rows
644 // are <form>, custom-theme rows are wrapper <div>s, and the
645 // Build-your-own tile is a <button>. Include ALL of these so the
646 // count of cards-per-row reflects what the user actually sees in
647 // the row, regardless of element type.
648 var cards = Array.prototype.slice.call(grid.children).filter(function(el) {
649 return el.nodeType === 1 && el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE';
650 });
651 if (!cards.length) return;
652 // Reset any prior pagination state so the row-count probe sees the
653 // real layout.
654 cards.forEach(function(c){ c.style.display = ''; });
655 var perRow = cardsPerRow(grid, cards);
656 var rowsToShow = rowsSettingFor(grid);
657 // rowsToShow=0 means "All" -- show every card, no pager.
658 var perPage = (rowsToShow === 0) ? cards.length : (perRow * rowsToShow);
659 if (cards.length <= perPage) {
660 // Remove any stale pager from a previous wider config.
661 var nextEl = grid.nextSibling;
662 if (nextEl && nextEl.classList && nextEl.classList.contains('pic-pager')) {
663 nextEl.parentNode.removeChild(nextEl);
664 }
665 return;
666 }
667 var pageCount = Math.ceil(cards.length / perPage);
668 var current = 0;
669 // If a stale pager exists (from a re-paginate after a setting
670 // change), reuse it instead of stacking another.
671 var pager;
672 var maybe = grid.nextSibling;
673 if (maybe && maybe.classList && maybe.classList.contains('pic-pager')) {
674 pager = maybe;
675 pager.innerHTML = '';
676 } else {
677 pager = document.createElement('div');
678 pager.className = 'pic-pager';
679 grid.parentNode.insertBefore(pager, grid.nextSibling);
680 }
681 function render() {
682 cards.forEach(function(c, i) {
683 var page = Math.floor(i / perPage);
684 c.style.display = (page === current) ? '' : 'none';
685 });
686 pager.innerHTML = '';
687 var prev = document.createElement('button');
688 prev.textContent = '<';
689 prev.disabled = current === 0;
690 prev.onclick = function() { current--; render(); grid.scrollIntoView({behavior:'smooth',block:'start'}); };
691 pager.appendChild(prev);
692 for (var p = 0; p < pageCount; p++) {
693 (function(p) {
694 var b = document.createElement('button');
695 b.textContent = (p + 1);
696 if (p === current) b.classList.add('active');
697 b.onclick = function() { current = p; render(); grid.scrollIntoView({behavior:'smooth',block:'start'}); };
698 pager.appendChild(b);
699 })(p);
700 }
701 var next = document.createElement('button');
702 next.textContent = '>';
703 next.disabled = current === pageCount - 1;
704 next.onclick = function() { current++; render(); grid.scrollIntoView({behavior:'smooth',block:'start'}); };
705 pager.appendChild(next);
706 var info = document.createElement('span');
707 info.className = 'pic-info';
708 info.textContent = 'Showing ' + (current * perPage + 1) + '-' + Math.min((current + 1) * perPage, cards.length) + ' of ' + cards.length;
709 pager.appendChild(info);
710 }
711 render();
712 // _repaginate just re-runs paginate(grid) from scratch -- handles
713 // resize, dropdown changes, and any other "config changed" event
714 // without trying to surgically mutate this closure's state.
715 grid._repaginate = function() { paginate(grid); };
716 }
717 function initPagers() {
718 // Sync each "Show N rows" select to its stored value, then paginate.
719 document.querySelectorAll('[data-rows-control]').forEach(function(sel) {
720 var key = sel.getAttribute('data-rows-control');
721 var stored = null;
722 try { stored = localStorage.getItem('ct_rows_' + key); } catch (e) {}
723 if (stored !== null) sel.value = stored;
724 sel.addEventListener('change', function() {
725 try { localStorage.setItem('ct_rows_' + key, sel.value); } catch (e) {}
726 var grid = document.querySelector('[data-rows-key="' + key + '"]');
727 if (grid && typeof grid._repaginate === 'function') grid._repaginate();
728 else if (grid) paginate(grid);
729 });
730 });
731 document.querySelectorAll('[data-picker-grid]').forEach(paginate);
732 }
733 var _resizeTimer = null;
734 function onResize() {
735 if (_resizeTimer) clearTimeout(_resizeTimer);
736 _resizeTimer = setTimeout(function() {
737 document.querySelectorAll('[data-picker-grid]').forEach(function(grid) {
738 if (typeof grid._repaginate === 'function') grid._repaginate();
739 });
740 }, 150);
741 }
742 // Wait for window.load (CSS + images) so the grid has truly laid out
743 // before we measure. DOMContentLoaded fires before stylesheets finish
744 // and the measurements can be zero -- which made every card report
745 // the same offsetTop and pagination never engaged.
746 function startPaging() {
747 // requestAnimationFrame gives the browser one more frame to commit
748 // layout, defending against the rare case where CSS is parsed but
749 // not yet painted.
750 if (window.requestAnimationFrame) {
751 window.requestAnimationFrame(initPagers);
752 } else {
753 initPagers();
754 }
755 }
756 if (document.readyState === 'complete') {
757 startPaging();
758 } else {
759 window.addEventListener('load', startPaging);
760 }
761 window.addEventListener('resize', onResize);
762})();
763</script>
764
765<!-- AJAX toggle for the page-in-menu pill. Saves the new state via
766 fetch and swaps the button label / colors in place so the seller
767 doesn't lose their scroll position. The form still submits
768 normally as a fallback when JS is disabled. -->
769<script>
770(function () {
771 // Inline SVGs as strings so we can swap them without re-fetching.
772 var EYE_ON = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
773 var EYE_OFF = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>';
774 var STYLE_ON = 'background:rgba(16,185,129,0.12);border:1px solid rgba(16,185,129,0.45);color:#6ee7b7;padding:6px 10px;vertical-align:middle';
775 var STYLE_OFF = 'background:transparent;border:1px solid var(--col-border);color:var(--col-text-3);padding:6px 10px;vertical-align:middle';
776
777 function paint(form, inMenu) {
778 var btn = form.querySelector('.pg-menu-btn');
779 var tgt = form.querySelector('[data-pg-target]');
780 if (!btn || !tgt) return;
781 if (inMenu) {
782 btn.className = 'btn btn-sm pg-menu-btn is-on';
783 btn.setAttribute('style', STYLE_ON);
784 btn.setAttribute('title', 'Currently shown in the storefront menu -- click to hide');
785 btn.innerHTML = EYE_ON + '<span data-pg-label>In menu</span>';
786 tgt.value = '0';
787 } else {
788 btn.className = 'btn btn-sm pg-menu-btn is-off';
789 btn.setAttribute('style', STYLE_OFF);
790 btn.setAttribute('title', 'Currently hidden from the storefront menu -- click to show');
791 btn.innerHTML = EYE_OFF + '<span data-pg-label>Hidden</span>';
792 tgt.value = '1';
793 }
794 }
795
796 document.querySelectorAll('form.pg-menu-form').forEach(function (form) {
797 form.addEventListener('submit', function (e) {
798 e.preventDefault();
799 var tgt = form.querySelector('[data-pg-target]');
800 var btn = form.querySelector('.pg-menu-btn');
801 if (!tgt) return;
802 var target = tgt.value; // '1' = turn ON, '0' = turn OFF
803 if (btn) btn.disabled = true;
804
805 var data = new FormData(form);
806 data.append('async', '1');
807
808 fetch('/storefront.cgi', { method: 'POST', body: data, credentials: 'same-origin' })
809 .then(function (r) { return r.ok ? r.json() : null; })
810 .then(function (j) {
811 if (btn) btn.disabled = false;
812 if (!j || !j.ok) {
813 appToast({ message: 'Could not update menu setting. Try again or refresh.', type: 'error' });
814 return;
815 }
816 paint(form, j.in_menu === 1);
817 })
818 .catch(function () {
819 if (btn) btn.disabled = false;
820 appToast({ message: 'Network error. Try again.', type: 'error' });
821 });
822 });
823 });
824})();
825</script>
826
827 <!-- ----- LAYOUT picker -------------------------------------------- -->
828 <div class="module" style="margin-bottom:24px">
829 <div class="module-head">
830 <div class="left">
831 <div class="module-icon">
832 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
833 </div>
834 <div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">
835 <div class="module-title" style="white-space:nowrap">1. Pick a layout</div>
836 <div class="module-sub" style="margin:0">The page structure &mdash; hero shape, product grid style, where about/reviews sit.</div>
837 </div>
838 </div>
839 <div class="ct-rows-ctrl">
840 <label class="text-xs text-secondary">Show
841 <select data-rows-control="layouts" class="ct-rows-select">
842 <option value="1">1 row</option>
843 <option value="2">2 rows</option>
844 <option value="3">3 rows</option>
845 <option value="0">All</option>
846 </select>
847 </label>
848 </div>
849 </div>
850
851 <div class="module-body">
852 [if:$layout_saved_flash]
853 <div class="banner success" style="margin-bottom:14px">
854 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
855 <div class="text-xs">Layout updated. Refresh your storefront to see the new structure.</div>
856 </div>
857 [/if]
858
859 <div class="banner info" style="margin-bottom:18px">
860 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
861 <div class="text-xs"><strong>Current layout:</strong> $layout_label &middot; $layout_tagline</div>
862 </div>
863
864 <div data-picker-grid data-rows-key="layouts" class="picker-grid-5">
865 [loop:@layouts]
866 <form method="POST" action="/storefront.cgi" class="$loop1.card_class" style="display:flex;flex-direction:column;border:1px solid var(--col-border);border-radius:14px;overflow:hidden;background:var(--col-surface-1);transition:border-color 0.15s">
867 <input type="hidden" name="action" value="layout">
868 <input type="hidden" name="store_id" value="$loop1.store_id">
869 <input type="hidden" name="layout_id" value="$loop1.id">
870
871 <div class="pic-thumb" style="height:220px;border-bottom:1px solid var(--col-border)">$loop1.thumbnail_html</div>
872
873 <div style="padding:16px;flex:1;display:flex;flex-direction:column;gap:8px">
874 <div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
875 <strong style="font-size:15px">$loop1.name</strong>
876 $loop1.active_badge
877 </div>
878 <p class="text-xs text-secondary" style="line-height:1.5;margin:0">$loop1.tagline</p>
879 <p class="text-xs text-dim" style="margin:0"><strong>Best for:</strong> $loop1.best_for</p>
880 <div style="display:flex;gap:8px;margin-top:auto;padding-top:6px">
881 <a href="$loop1.preview_url" target="_blank" rel="noopener" class="btn btn-secondary btn-sm" style="flex:1;text-align:center" title="Open your storefront with this layout in a new tab">
882 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
883 Preview
884 </a>
885 <button type="submit" class="$loop1.button_class" $loop1.button_disabled style="flex:1">$loop1.button_label</button>
886 </div>
887 </div>
888 </form>
889 [/loop]
890 </div>
891
892 <div class="text-xs text-dim mt-3" style="line-height:1.6">
893 <strong>For the curious:</strong> layouts live in <code>MODS/WebSTLs/Layouts.pm</code> + a template file in <code>TEMPLATES/store_layouts/</code>. Drop in a new template, add a row to the registry, and it shows up here.
894 </div>
895 </div>
896 </div>
897
898
899 <!-- ----- COLOR THEME picker --------------------------------------- -->
900 <div class="module">
901 <div class="module-head">
902 <div class="left">
903 <div class="module-icon">
904 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/><path d="M12 2a10 10 0 0 0 0 20c1.66 0 3-1.34 3-3 0-.78-.3-1.5-.8-2.04-.5-.5-.8-1.22-.8-2 0-1.66 1.34-3 3-3h2c2.76 0 5-2.24 5-5 0-4.97-4.5-9-10-9z"/></svg>
905 </div>
906 <div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">
907 <div class="module-title" style="white-space:nowrap">2. Pick a color theme</div>
908 <div class="module-sub" style="margin:0">The colors. Any color theme works with any layout.</div>
909 </div>
910 </div>
911 <div class="ct-rows-ctrl">
912 <label class="text-xs text-secondary">Show
913 <select data-rows-control="themes" class="ct-rows-select">
914 <option value="1">1 row</option>
915 <option value="2">2 rows</option>
916 <option value="3">3 rows</option>
917 <option value="0">All</option>
918 </select>
919 </label>
920 </div>
921 </div>
922
923 <div class="module-body">
924 [if:$theme_saved_flash]
925 <div class="banner success" style="margin-bottom:14px">
926 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
927 <div class="text-xs">Theme updated. Refresh your storefront to see the new colors.</div>
928 </div>
929 [/if]
930
931 <div class="banner info" style="margin-bottom:18px">
932 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
933 <div class="text-xs"><strong>Current theme:</strong> $theme_label &middot; $theme_tagline</div>
934 </div>
935
936 <div data-picker-grid data-rows-key="themes" class="picker-grid-5">
937 <!-- Build-your-own theme card. Opens the builder section below. -->
938 <button type="button" onclick="document.getElementById('ct-builder').scrollIntoView({behavior:'smooth'});document.getElementById('ct-builder').setAttribute('data-open','1');document.getElementById('ct-builder-body').style.display='grid';document.getElementById('ct-open-btn').style.display='none';document.getElementById('ct-close-btn').style.display='inline-flex';document.getElementById('ct-existing-id').value='';document.getElementById('ct-name').value='My Custom Theme';document.getElementById('ct-builder-title').textContent='Create custom theme';ctSyncSaveAsNew();" style="display:flex;flex-direction:column;border:2px dashed var(--col-accent);border-radius:14px;overflow:hidden;background:var(--col-surface-1);transition:all 0.15s;cursor:pointer;font-family:inherit;text-align:left;padding:0">
939 <div style="height:220px;border-bottom:1px solid var(--col-border);display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,var(--col-accent-glow),transparent 70%)">
940 <div style="text-align:center;color:var(--col-accent-bright)">
941 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:42px;height:42px;margin:0 auto 10px;display:block"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="19" cy="12" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="10" cy="20" r="2.5"/><path d="M12 22c-5 0-9-4-9-9 0-4 4-7 8-6 1.2.3 2 1 2 2 0 1-1 2-1 3 0 1 1 2 2 2h2c2 0 4-2 4-4 0-1-1-3-3-3a9 9 0 0 0-5 17z"/></svg>
942 <div style="font-size:16px;font-weight:700;letter-spacing:0.3px">Build your own</div>
943 <div class="text-xs text-secondary" style="margin-top:6px">Color picker for every surface</div>
944 </div>
945 </div>
946 <div style="padding:16px;flex:1">
947 <strong style="font-size:15px;color:var(--col-text)">Custom theme</strong>
948 <p class="text-xs text-secondary" style="line-height:1.5;margin:6px 0 0">Pick every color yourself. Live preview as you tweak.</p>
949 </div>
950 </button>
951
952 [loop:@themes]
953 <form method="POST" action="/storefront.cgi" class="$loop1.card_class" style="display:flex;flex-direction:column;border:1px solid var(--col-border);border-radius:14px;overflow:hidden;background:var(--col-surface-1);transition:border-color 0.15s">
954 <input type="hidden" name="action" value="theme">
955 <input type="hidden" name="store_id" value="$loop1.store_id">
956 <input type="hidden" name="theme_id" value="$loop1.id">
957
958 <div class="pic-thumb" style="height:220px;border-bottom:1px solid var(--col-border)">$loop1.thumbnail_html</div>
959
960 <div style="padding:16px;flex:1;display:flex;flex-direction:column;gap:8px">
961 <div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
962 <strong style="font-size:15px">$loop1.name</strong>
963 $loop1.active_badge
964 </div>
965 <p class="text-xs text-secondary" style="line-height:1.5;flex:1;margin:0">$loop1.tagline</p>
966 <div style="display:flex;gap:8px;margin-top:6px">
967 <a href="$loop1.preview_url" target="_blank" rel="noopener" class="btn btn-secondary btn-sm" style="flex:1;text-align:center" title="Open your storefront with this theme in a new tab">
968 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/></svg>
969 Preview
970 </a>
971 <button type="submit" class="$loop1.button_class" $loop1.button_disabled style="flex:1">$loop1.button_label</button>
972 </div>
973 </div>
974 </form>
975 [/loop]
976 </div>
977
978 [if:$has_custom_themes]
979 <h3 style="font-family:var(--font-display);font-size:16px;color:var(--col-text);margin:32px 0 14px;letter-spacing:0.3px">Your custom themes</h3>
980 <div data-picker-grid style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:18px">
981 [loop:@custom_themes]
982 [if:$loop1.is_active]
983 <div class="ct-active-wrap">
984 <span class="ct-active-tag">Current theme</span>
985 <div class="$loop1.card_class" style="display:flex;flex-direction:column;border:1px solid var(--col-accent);border-radius:11px;overflow:hidden;background:var(--col-surface-1)">
986 <div class="pic-thumb" style="height:220px;border-bottom:1px solid var(--col-border)">$loop1.thumbnail_html</div>
987 <div class="ct-swatches" title="All 12 colors in this theme">
988 <span class="ct-sw" style="background:$loop1.col_bg"></span>
989 <span class="ct-sw" style="background:$loop1.col_bg_2"></span>
990 <span class="ct-sw" style="background:$loop1.col_surface"></span>
991 <span class="ct-sw" style="background:$loop1.col_surface_2"></span>
992 <span class="ct-sw" style="background:$loop1.col_border"></span>
993 <span class="ct-sw" style="background:$loop1.col_border_2"></span>
994 <span class="ct-sw" style="background:$loop1.col_text"></span>
995 <span class="ct-sw" style="background:$loop1.col_text_2"></span>
996 <span class="ct-sw" style="background:$loop1.col_text_3"></span>
997 <span class="ct-sw" style="background:$loop1.col_accent"></span>
998 <span class="ct-sw" style="background:$loop1.col_accent_2"></span>
999 <span class="ct-sw" style="background:$loop1.col_accent_deep"></span>
1000 </div>
1001 <div style="padding:16px;flex:1;display:flex;flex-direction:column;gap:8px">
1002 <div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
1003 <strong style="font-size:15px">$loop1.name</strong>
1004 $loop1.active_badge
1005 </div>
1006 <p class="text-xs text-secondary" style="line-height:1.5;flex:1;margin:0">$loop1.tagline</p>
1007 <div style="display:flex;gap:8px;margin-top:6px;flex-wrap:wrap">
1008 <a href="$loop1.preview_url" target="_blank" rel="noopener" class="btn btn-secondary btn-sm" style="flex:1;text-align:center;min-width:80px">Preview</a>
1009 <button type="button" class="btn btn-secondary btn-sm" style="flex:1;min-width:60px"
1010 onclick="ctLoadIntoBuilder($loop1.id, '$loop1.name', '$loop1.col_bg', '$loop1.col_bg_2', '$loop1.col_surface', '$loop1.col_surface_2', '$loop1.col_border', '$loop1.col_border_2', '$loop1.col_text', '$loop1.col_text_2', '$loop1.col_text_3', '$loop1.col_accent', '$loop1.col_accent_2', '$loop1.col_accent_deep', '$loop1.col_accent_glow')">Edit</button>
1011 <form method="POST" action="/storefront.cgi" style="flex:0 0 auto;margin:0"
1012 data-confirm-title="Delete this custom theme?"
1013 data-confirm-body="The theme will be permanently removed. This cannot be undone."
1014 data-confirm-ok="Delete theme"
1015 data-confirm-tone="danger">
1016 <input type="hidden" name="action" value="delete_custom_theme">
1017 <input type="hidden" name="store_id" value="$loop1.store_id">
1018 <input type="hidden" name="custom_theme_id" value="$loop1.id">
1019 <button type="submit" class="btn btn-sm" style="background:transparent;border:1px solid rgba(239,68,68,0.45);color:#f87171;padding:6px 10px" title="Delete this custom theme">
1020 <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
1021 </button>
1022 </form>
1023 </div>
1024 </div>
1025 </div>
1026 </div>
1027 [/if]
1028 [if:!$loop1.is_active]
1029 <div class="ct-inactive-wrap">
1030 <span class="ct-inactive-tag">InActive</span>
1031 <div class="$loop1.card_class" style="display:flex;flex-direction:column;border:1px solid rgba(59,130,246,0.40);border-radius:11px;overflow:hidden;background:var(--col-surface-1)">
1032 <div class="pic-thumb" style="height:220px;border-bottom:1px solid var(--col-border)">$loop1.thumbnail_html</div>
1033 <div class="ct-swatches" title="All 12 colors in this theme">
1034 <span class="ct-sw" style="background:$loop1.col_bg"></span>
1035 <span class="ct-sw" style="background:$loop1.col_bg_2"></span>
1036 <span class="ct-sw" style="background:$loop1.col_surface"></span>
1037 <span class="ct-sw" style="background:$loop1.col_surface_2"></span>
1038 <span class="ct-sw" style="background:$loop1.col_border"></span>
1039 <span class="ct-sw" style="background:$loop1.col_border_2"></span>
1040 <span class="ct-sw" style="background:$loop1.col_text"></span>
1041 <span class="ct-sw" style="background:$loop1.col_text_2"></span>
1042 <span class="ct-sw" style="background:$loop1.col_text_3"></span>
1043 <span class="ct-sw" style="background:$loop1.col_accent"></span>
1044 <span class="ct-sw" style="background:$loop1.col_accent_2"></span>
1045 <span class="ct-sw" style="background:$loop1.col_accent_deep"></span>
1046 </div>
1047 <div style="padding:16px;flex:1;display:flex;flex-direction:column;gap:8px">
1048 <div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
1049 <strong style="font-size:15px">$loop1.name</strong>
1050 $loop1.active_badge
1051 </div>
1052 <p class="text-xs text-secondary" style="line-height:1.5;flex:1;margin:0">$loop1.tagline</p>
1053 <div style="display:flex;gap:8px;margin-top:6px;flex-wrap:wrap">
1054 <a href="$loop1.preview_url" target="_blank" rel="noopener" class="btn btn-secondary btn-sm" style="flex:1;text-align:center;min-width:80px">Preview</a>
1055 <button type="button" class="btn btn-secondary btn-sm" style="flex:1;min-width:60px"
1056 onclick="ctLoadIntoBuilder($loop1.id, '$loop1.name', '$loop1.col_bg', '$loop1.col_bg_2', '$loop1.col_surface', '$loop1.col_surface_2', '$loop1.col_border', '$loop1.col_border_2', '$loop1.col_text', '$loop1.col_text_2', '$loop1.col_text_3', '$loop1.col_accent', '$loop1.col_accent_2', '$loop1.col_accent_deep', '$loop1.col_accent_glow')">Edit</button>
1057 <form method="POST" action="/storefront.cgi" style="flex:0 0 auto;margin:0"
1058 data-confirm-title="Delete this custom theme?"
1059 data-confirm-body="The theme will be permanently removed. This cannot be undone."
1060 data-confirm-ok="Delete theme"
1061 data-confirm-tone="danger">
1062 <input type="hidden" name="action" value="delete_custom_theme">
1063 <input type="hidden" name="store_id" value="$loop1.store_id">
1064 <input type="hidden" name="custom_theme_id" value="$loop1.id">
1065 <button type="submit" class="btn btn-sm" style="background:transparent;border:1px solid rgba(239,68,68,0.45);color:#f87171;padding:6px 10px" title="Delete this custom theme">
1066 <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
1067 </button>
1068 </form>
1069 </div>
1070 </div>
1071 </div>
1072 </div>
1073 [/if]
1074 [/loop]
1075 </div>
1076 [/if]
1077
1078 <!-- ============ CUSTOM THEME BUILDER ============ -->
1079 <div id="ct-builder" data-open="0" style="margin-top:36px;border:1px solid var(--col-accent);border-radius:14px;background:var(--col-surface-1);overflow:hidden">
1080 <div style="padding:18px 22px;background:linear-gradient(135deg,var(--col-accent-glow),transparent 70%);border-bottom:1px solid var(--col-border);display:flex;align-items:center;justify-content:space-between;gap:14px;flex-wrap:wrap">
1081 <div>
1082 <div class="text-xs" style="letter-spacing:1.5px;text-transform:uppercase;color:var(--col-accent-bright);font-weight:700">Custom theme builder</div>
1083 <div id="ct-builder-title" style="font-family:var(--font-display);font-size:20px;color:var(--col-text);margin-top:4px">Create custom theme</div>
1084 </div>
1085 <button type="button" onclick="document.getElementById('ct-builder').setAttribute('data-open','0');document.getElementById('ct-builder-body').style.display='none';this.style.display='none';document.getElementById('ct-open-btn').style.display='inline-flex';" id="ct-close-btn" class="help-btn" style="display:none">
1086 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="6" y1="6" x2="18" y2="18"/><line x1="6" y1="18" x2="18" y2="6"/></svg>
1087 <span>Close</span>
1088 </button>
1089 <button type="button" onclick="document.getElementById('ct-builder').setAttribute('data-open','1');document.getElementById('ct-builder-body').style.display='grid';this.style.display='none';document.getElementById('ct-close-btn').style.display='inline-flex';" id="ct-open-btn" class="help-btn">
1090 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="19" cy="12" r="2.5"/><circle cx="6" cy="12" r="2.5"/><circle cx="10" cy="20" r="2.5"/><path d="M12 22c-5 0-9-4-9-9 0-4 4-7 8-6 1.2.3 2 1 2 2 0 1-1 2-1 3 0 1 1 2 2 2h2c2 0 4-2 4-4 0-1-1-3-3-3a9 9 0 0 0-5 17z"/></svg>
1091 <span>Open builder</span>
1092 </button>
1093 </div>
1094
1095 <form id="ct-builder-body" method="POST" action="/storefront.cgi" style="display:none;grid-template-columns:1.1fr 1fr;gap:0;align-items:start">
1096 <input type="hidden" name="action" value="save_custom_theme">
1097 <input type="hidden" name="store_id" value="$store_id">
1098 <input type="hidden" name="existing_id" id="ct-existing-id" value="">
1099
1100 <div style="padding:22px;border-right:1px solid var(--col-border)">
1101 <label class="form-label" style="font-size:11px">Theme name</label>
1102 <input type="text" name="theme_name" id="ct-name" value="My Custom Theme" maxlength="80" class="input" style="width:100%;margin-bottom:18px">
1103
1104 <div class="text-xs" style="letter-spacing:1.2px;text-transform:uppercase;color:var(--col-text-3);font-weight:700;margin-bottom:8px">Accent (your brand color)</div>
1105 <div class="ct-row"><div class="ct-left"><label class="ct-label">Primary</label><span class="ct-hint">Buttons, links, the &quot;you&quot; color &mdash; your dominant brand hue</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('accent')" title="Reset just this color">Reset</button><input type="color" name="col_accent" id="ctp_accent" value="$seed_accent" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1106 <div class="ct-row"><div class="ct-left"><label class="ct-label">Bright (highlight)</label><span class="ct-hint">Gradient end-stop &mdash; the lighter half of button &amp; logo gradients</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('accent_2')" title="Reset just this color">Reset</button><input type="color" name="col_accent_2" id="ctp_accent_2" value="$seed_accent_2" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1107 <div class="ct-row"><div class="ct-left"><label class="ct-label">Deep (pressed)</label><span class="ct-hint">Darker variant for button hover and pressed states</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('accent_deep')" title="Reset just this color">Reset</button><input type="color" name="col_accent_deep" id="ctp_accent_deep" value="$seed_accent_deep" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1108 <input type="hidden" name="col_accent_glow" id="ctp_accent_glow" value="$seed_accent_glow">
1109
1110 <div class="text-xs" style="letter-spacing:1.2px;text-transform:uppercase;color:var(--col-text-3);font-weight:700;margin:18px 0 8px">Backgrounds</div>
1111 <div class="ct-row"><div class="ct-left"><label class="ct-label">Page</label><span class="ct-hint">Main canvas behind everything &mdash; the body color</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('bg')" title="Reset just this color">Reset</button><input type="color" name="col_bg" id="ctp_bg" value="$seed_bg" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1112 <div class="ct-row"><div class="ct-left"><label class="ct-label">Topbar</label><span class="ct-hint">Sticky header bar at the very top of your storefront</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('bg_2')" title="Reset just this color">Reset</button><input type="color" name="col_bg_2" id="ctp_bg_2" value="$seed_bg_2" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1113 <div class="ct-row"><div class="ct-left"><label class="ct-label">Surface</label><span class="ct-hint">Product cards, panels, every box that floats above the page</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('surface')" title="Reset just this color">Reset</button><input type="color" name="col_surface" id="ctp_surface" value="$seed_surface" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1114 <div class="ct-row"><div class="ct-left"><label class="ct-label">Surface 2</label><span class="ct-hint">Elevated panels &mdash; modals, dropdowns, secondary sections</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('surface_2')" title="Reset just this color">Reset</button><input type="color" name="col_surface_2" id="ctp_surface_2" value="$seed_surface_2" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1115
1116 <div class="text-xs" style="letter-spacing:1.2px;text-transform:uppercase;color:var(--col-text-3);font-weight:700;margin:18px 0 8px">Borders</div>
1117 <div class="ct-row"><div class="ct-left"><label class="ct-label">Border</label><span class="ct-hint">Thin 1px lines around cards, table rows, dividers</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('border')" title="Reset just this color">Reset</button><input type="color" name="col_border" id="ctp_border" value="$seed_border" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1118 <div class="ct-row"><div class="ct-left"><label class="ct-label">Border 2</label><span class="ct-hint">Stronger borders for hover, focus, and active states</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('border_2')" title="Reset just this color">Reset</button><input type="color" name="col_border_2" id="ctp_border_2" value="$seed_border_2" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1119
1120 <div class="text-xs" style="letter-spacing:1.2px;text-transform:uppercase;color:var(--col-text-3);font-weight:700;margin:18px 0 8px">Text</div>
1121 <div class="ct-row"><div class="ct-left"><label class="ct-label">Primary</label><span class="ct-hint">Headings, product names, the main readable copy</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('text')" title="Reset just this color">Reset</button><input type="color" name="col_text" id="ctp_text" value="$seed_text" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1122 <div class="ct-row"><div class="ct-left"><label class="ct-label">Muted</label><span class="ct-hint">Paragraph copy, descriptions, secondary text</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('text_2')" title="Reset just this color">Reset</button><input type="color" name="col_text_2" id="ctp_text_2" value="$seed_text_2" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1123 <div class="ct-row"><div class="ct-left"><label class="ct-label">Dim</label><span class="ct-hint">Faint labels, timestamps, low-emphasis hints</span></div><div class="ct-right"><button type="button" class="ct-reset" onclick="ctResetOne('text_3')" title="Reset just this color">Reset</button><input type="color" name="col_text_3" id="ctp_text_3" value="$seed_text_3" class="ct-color" oninput="ctUpdatePreview()"></div></div>
1124
1125 <div style="display:flex;gap:10px;margin-top:24px;flex-wrap:wrap">
1126 <button type="submit" class="btn btn-primary" style="flex:1 1 180px">Save &amp; apply</button>
1127 <button type="button" id="ct-save-as-new" onclick="ctSaveAsNew()" class="btn btn-secondary" style="display:none;flex:1 1 140px" title="Save these colors as a brand-new theme without overwriting the current one">Save as new</button>
1128 <button type="button" onclick="ctResetFromSeed()" class="btn btn-secondary" style="flex:0 0 auto">Reset all</button>
1129 </div>
1130 </div>
1131
1132 <div style="padding:22px;background:var(--col-surface-2);position:sticky;top:20px">
1133 <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px">
1134 <div class="text-xs" style="letter-spacing:1.2px;text-transform:uppercase;color:var(--col-text-3);font-weight:700">Live preview</div>
1135 <a id="ct-preview-open" href="#" target="_blank" rel="noopener" class="text-xs" style="color:var(--col-accent-bright);text-decoration:none;font-weight:600" title="Open this preview in a new tab at full size">Open full &rarr;</a>
1136 </div>
1137 <div style="border-radius:10px;overflow:hidden;border:1px solid var(--col-border);background:var(--col-bg);height:540px;position:relative">
1138 <iframe id="ct-preview-iframe" src="about:blank" style="width:100%;height:100%;border:0;display:block" title="Live storefront preview"></iframe>
1139 <div id="ct-preview-loading" style="position:absolute;top:8px;right:10px;padding:3px 8px;border-radius:6px;background:rgba(15,23,42,0.75);color:#cbd5e1;font-size:10px;letter-spacing:0.8px;text-transform:uppercase;font-weight:700;display:none">Updating&hellip;</div>
1140 </div>
1141 <p class="text-xs text-secondary" style="margin-top:12px;line-height:1.5">Real storefront with your colors applied. Save &amp; apply writes the theme to your storefront and reloads the picker.</p>
1142 </div>
1143 </form>
1144 </div>
1145
1146 <style>
1147 /* Layout and theme pickers are locked to 5 columns at desktop
1148 widths -- auto-fill was unreliable because Windows display
1149 scaling shrinks the effective viewport enough that auto-fill
1150 would drop to 4 columns even though the user asked for 5.
1151 Force the column count instead so it's predictable. Step
1152 down on smaller viewports where 5 columns would crush cards. */
1153 .picker-grid-5 { display:grid; grid-template-columns:repeat(5, minmax(0, 1fr)); gap:18px; }
1154 /* Step down only at narrow widths -- the previous 1200px cutoff
1155 was being tripped by Windows display scaling (a 1920px screen
1156 at 150% reports as 1280px CSS, which dropped to 4 cols on the
1157 wrong viewports). Hold 5 columns down to ~850px. */
1158 @media (max-width: 850px) { .picker-grid-5 { grid-template-columns:repeat(3, minmax(0, 1fr)); } }
1159 @media (max-width: 650px) { .picker-grid-5 { grid-template-columns:repeat(2, minmax(0, 1fr)); } }
1160 @media (max-width: 450px) { .picker-grid-5 { grid-template-columns:1fr; } }
1161 /* "Show N rows" dropdown that sits to the right of the section
1162 title in the layout / theme picker. Choice is persisted to
1163 localStorage and applies on every page load. */
1164 .ct-rows-ctrl { margin-left:auto; display:flex; align-items:center; gap:6px; }
1165 .ct-rows-ctrl label { display:flex; align-items:center; gap:8px; font-weight:600; letter-spacing:0.4px; text-transform:uppercase; font-size:11px; }
1166 .ct-rows-select { background:var(--col-surface-2); border:1px solid var(--col-border); color:var(--col-text); border-radius:8px; padding:6px 10px; font-size:12px; font-family:inherit; cursor:pointer; }
1167 .ct-rows-select:hover { border-color:var(--col-border-2); }
1168 .ct-rows-select:focus { outline:2px solid var(--col-accent); outline-offset:1px; }
1169 .ct-row { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 0; border-bottom:1px solid var(--col-border); }
1170 .ct-row:last-child { border-bottom:none; }
1171 .ct-left { display:flex; flex-direction:column; gap:2px; min-width:0; flex:1; }
1172 .ct-label { font-size:13px; color:var(--col-text-2); font-weight:600; }
1173 .ct-hint { font-size:11px; color:var(--col-text-3); line-height:1.35; }
1174 .ct-right { display:flex; align-items:center; gap:8px; flex-shrink:0; }
1175 .ct-color { width:46px; height:30px; border:1px solid var(--col-border); border-radius:6px; cursor:pointer; padding:0; background:transparent; }
1176 .ct-reset { font-size:10px; letter-spacing:0.6px; text-transform:uppercase; font-weight:600; padding:4px 9px; border-radius:5px; background:transparent; border:1px solid var(--col-border); color:var(--col-text-3); cursor:pointer; font-family:inherit; transition:background 0.12s, color 0.12s, border-color 0.12s; }
1177 .ct-reset:hover { background:var(--col-surface-2); color:var(--col-text); border-color:var(--col-border-2); }
1178 /* Wrapper for the active custom theme card. The frame + label
1179 make "this is the live one" obvious at a glance, without the
1180 extra disabled button hogging the action row. */
1181 .ct-active-wrap { position:relative; padding:18px 14px 14px; border:1px solid var(--col-accent); border-radius:16px; background:linear-gradient(135deg, var(--col-accent-glow), transparent 60%); box-shadow:0 0 24px rgba(124,58,237,0.20); }
1182 .ct-active-tag { position:absolute; top:-9px; left:18px; padding:2px 10px; background:var(--col-bg); border:1px solid var(--col-accent); border-radius:999px; font-size:10px; letter-spacing:1.4px; text-transform:uppercase; font-weight:700; color:var(--col-accent-bright); }
1183 /* Inactive variant: same shape & corner-tag pattern as active,
1184 painted in blue so the two states sit visually parallel in
1185 the grid. The corner tag is text-only -- activation still
1186 happens via the inline "Inactive" pill next to the title. */
1187 .ct-inactive-wrap { position:relative; padding:18px 14px 14px; border:1px solid rgba(59,130,246,0.55); border-radius:16px; background:linear-gradient(135deg, rgba(59,130,246,0.10), transparent 60%); box-shadow:0 0 18px rgba(59,130,246,0.15); }
1188 .ct-inactive-tag { position:absolute; top:-9px; left:18px; padding:2px 10px; background:var(--col-bg); border:1px solid rgba(59,130,246,0.55); border-radius:999px; font-size:10px; letter-spacing:1.4px; text-transform:none; font-weight:700; color:#93c5fd; }
1189 /* Blue "Inactive" pill mirrors the green Active pill's position
1190 and shape but is ALSO a submit button -- clicking it activates
1191 the theme on the storefront. */
1192 .pill-inactive { display:inline-block; padding:3px 11px; border-radius:999px; background:rgba(59,130,246,0.14); border:1px solid rgba(59,130,246,0.55); color:#93c5fd; font-size:11px; font-weight:700; letter-spacing:0.3px; line-height:1.2; transition:background 0.12s, color 0.12s, transform 0.12s, box-shadow 0.12s; }
1193 .pill-inactive:hover { background:rgba(59,130,246,0.28); color:#fff; box-shadow:0 0 14px rgba(59,130,246,0.45); transform:translateY(-1px); }
1194 /* Color swatch strip under the thumbnail. 12 mini squares
1195 showing every color in the theme so users can identify a
1196 palette at a glance without opening it. */
1197 .ct-swatches { display:flex; gap:2px; padding:6px 10px; background:var(--col-bg-2); border-bottom:1px solid var(--col-border); align-items:center; }
1198 .ct-swatches .ct-sw { flex:1; height:14px; border-radius:2px; border:1px solid rgba(255,255,255,0.06); }
1199 /* Brief flash on the builder section when the user clicks Edit
1200 so it's obvious the form below now reflects their click --
1201 otherwise the scroll alone is too subtle to notice. */
1202 @keyframes ct-builder-flash {
1203 0% { box-shadow: 0 0 0 0 rgba(124,58,237,0); }
1204 15% { box-shadow: 0 0 0 4px rgba(124,58,237,0.55), 0 0 40px rgba(124,58,237,0.45); }
1205 100% { box-shadow: 0 0 0 0 rgba(124,58,237,0); }
1206 }
1207 .ct-builder-flash { animation: ct-builder-flash 0.9s ease-out; }
1208 </style>
1209
1210 <script>
1211 // Hex -> "r,g,b" so we can construct the accent_glow rgba on the fly.
1212 function ctHexToRgb(h) {
1213 h = (h || '').replace('#', '');
1214 if (h.length === 3) h = h.split('').map(function(c){return c+c;}).join('');
1215 var n = parseInt(h, 16);
1216 return [ (n >> 16) & 255, (n >> 8) & 255, n & 255 ];
1217 }
1218 // Color pickers fire 'input' continuously while the user drags --
1219 // reloading the iframe on every event would thrash the network and
1220 // never let the preview settle. Debounce ~350ms so the iframe
1221 // reloads once the user pauses on a color.
1222 var _ctPreviewTimer = null;
1223 function ctUpdatePreview() {
1224 var fields = ['bg','bg_2','surface','surface_2','border','border_2','text','text_2','text_3','accent','accent_2','accent_deep'];
1225 var vals = {};
1226 fields.forEach(function(f){ vals[f] = document.getElementById('ctp_' + f).value; });
1227 // Derive accent_glow from accent at 35% alpha so creators don't have to think about it.
1228 var rgb = ctHexToRgb(vals.accent);
1229 var glow = 'rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ',0.35)';
1230 document.getElementById('ctp_accent_glow').value = glow;
1231
1232 if (_ctPreviewTimer) clearTimeout(_ctPreviewTimer);
1233 var loading = document.getElementById('ct-preview-loading');
1234 if (loading) loading.style.display = 'block';
1235 _ctPreviewTimer = setTimeout(function () {
1236 // Build the storefront preview URL with the picker colors as
1237 // query params. store.cgi reads pc_* and renders the seller's
1238 // real storefront with those colors injected as CSS vars --
1239 // so they see their own products + the new palette.
1240 var sid = $store_id;
1241 var qs = 'preview_colors=1';
1242 fields.forEach(function(f){
1243 qs += '&pc_' + f + '=' + encodeURIComponent(vals[f]);
1244 });
1245 qs += '&pc_accent_glow=' + encodeURIComponent(glow);
1246 var url = '/store.cgi?id=' + sid + '&' + qs;
1247 var iframe = document.getElementById('ct-preview-iframe');
1248 iframe.onload = function () { if (loading) loading.style.display = 'none'; };
1249 iframe.src = url;
1250 var openLink = document.getElementById('ct-preview-open');
1251 if (openLink) openLink.href = url;
1252 }, 350);
1253 }
1254 // Single source of truth for "what does Reset reset TO" -- the
1255 // page-load seed values rendered by Perl. Both ctResetOne and
1256 // ctResetFromSeed read from this same object.
1257 var ctSeed = {
1258 bg:'$seed_bg', bg_2:'$seed_bg_2', surface:'$seed_surface', surface_2:'$seed_surface_2',
1259 border:'$seed_border', border_2:'$seed_border_2',
1260 text:'$seed_text', text_2:'$seed_text_2', text_3:'$seed_text_3',
1261 accent:'$seed_accent', accent_2:'$seed_accent_2', accent_deep:'$seed_accent_deep'
1262 };
1263 function ctResetOne(field) {
1264 // Reset just one color back to its seed value, then refresh the
1265 // preview. Lets the user A/B a single picker without losing the
1266 // rest of their tweaks.
1267 if (!ctSeed.hasOwnProperty(field)) return;
1268 var el = document.getElementById('ctp_' + field);
1269 if (!el) return;
1270 el.value = ctSeed[field];
1271 ctUpdatePreview();
1272 }
1273 function ctResetFromSeed() {
1274 // Reset all pickers back to the page-load seed values.
1275 Object.keys(ctSeed).forEach(function(k){ document.getElementById('ctp_' + k).value = ctSeed[k]; });
1276 ctUpdatePreview();
1277 }
1278 // "Save as new" only makes sense when editing an existing theme.
1279 // Wire it to mirror existing_id state: visible while editing,
1280 // hidden in create-fresh mode. Centralized here so every code
1281 // path that toggles mode (Edit, Build your own, after-save reload)
1282 // gets the right visibility for free.
1283 function ctSyncSaveAsNew() {
1284 var ex = document.getElementById('ct-existing-id');
1285 var btn = document.getElementById('ct-save-as-new');
1286 if (!ex || !btn) return;
1287 btn.style.display = (ex.value && ex.value !== '') ? 'inline-flex' : 'none';
1288 }
1289 function ctSaveAsNew() {
1290 // Clone the current builder state into a new row by clearing
1291 // existing_id (so the server takes the INSERT branch). Auto-
1292 // append " (copy)" to the name so the user has two distinct
1293 // entries in their theme collection.
1294 var ex = document.getElementById('ct-existing-id');
1295 var name = document.getElementById('ct-name');
1296 if (ex) ex.value = '';
1297 // indexOf instead of a regex with \( -- Perl's qq~~ eats the
1298 // backslash so the rendered JS becomes /(copy/i which is an
1299 // unterminated regex group and crashes the whole <script>.
1300 if (name && name.value && name.value.toLowerCase().indexOf('(copy') < 0) {
1301 name.value = name.value + ' (copy)';
1302 }
1303 // Submit the form -- backend treats empty existing_id as INSERT.
1304 document.getElementById('ct-builder-body').submit();
1305 }
1306 // Reused by ctLoadIntoBuilder and any other "click came in, make
1307 // sure the builder is visible" path. Idempotent -- safe to call
1308 // when the builder is already open.
1309 function ctOpenBuilder() {
1310 var builder = document.getElementById('ct-builder');
1311 var body = document.getElementById('ct-builder-body');
1312 var openBtn = document.getElementById('ct-open-btn');
1313 var closeBtn= document.getElementById('ct-close-btn');
1314 if (builder) builder.setAttribute('data-open','1');
1315 if (body) body.style.display = 'grid';
1316 if (openBtn) openBtn.style.display = 'none';
1317 if (closeBtn) closeBtn.style.display = 'inline-flex';
1318 if (builder) {
1319 builder.scrollIntoView({behavior:'smooth', block:'start'});
1320 builder.classList.remove('ct-builder-flash');
1321 void builder.offsetWidth; // reflow so the animation re-runs
1322 builder.classList.add('ct-builder-flash');
1323 }
1324 }
1325 function ctLoadIntoBuilder(id, name, bg, bg_2, surface, surface_2, border, border_2, text, text_2, text_3, accent, accent_2, accent_deep, accent_glow) {
1326 // Open the builder FIRST. If anything below throws (a missing
1327 // picker, a weird value, whatever) the form is at least visible
1328 // so the user can see something happened.
1329 ctOpenBuilder();
1330 try {
1331 var ex = document.getElementById('ct-existing-id');
1332 var nameEl = document.getElementById('ct-name');
1333 var titleEl= document.getElementById('ct-builder-title');
1334 if (ex) ex.value = id;
1335 if (nameEl) nameEl.value = name;
1336 if (titleEl) titleEl.textContent = 'Edit: ' + name;
1337 // input type=color only accepts 7-char hex; trim longer forms.
1338 function trim7(h) { return (h && h.charAt(0) === '#' && h.length >= 7) ? h.substring(0,7) : (h || '#000000'); }
1339 var fields = {
1340 bg: bg, bg_2: bg_2, surface: surface, surface_2: surface_2,
1341 border: border, border_2: border_2,
1342 text: text, text_2: text_2, text_3: text_3,
1343 accent: accent, accent_2: accent_2, accent_deep: accent_deep
1344 };
1345 Object.keys(fields).forEach(function (k) {
1346 var el = document.getElementById('ctp_' + k);
1347 if (el) el.value = trim7(fields[k]);
1348 });
1349 var glowEl = document.getElementById('ctp_accent_glow');
1350 if (glowEl && accent_glow) glowEl.value = accent_glow;
1351 } catch (err) {
1352 // Don't let a single picker hiccup hide the builder.
1353 if (window.console) console.error('ctLoadIntoBuilder:', err);
1354 }
1355 ctSyncSaveAsNew();
1356 ctUpdatePreview();
1357 }
1358 // Initialize the preview on page load.
1359 ctSyncSaveAsNew();
1360 ctUpdatePreview();
1361 </script>
1362
1363 <div class="text-xs text-dim mt-3" style="line-height:1.6">
1364 <strong>For the curious:</strong> built-in themes live in <code>MODS/WebSTLs/Themes.pm</code>. Custom themes save to <code>storefront_custom_themes</code> and load via <code>resolve_for_storefront()</code>.
1365 </div>
1366 </div>
1367 </div>
1368</div>
1369
1370
1371<!-- ===== Domain & SSL =============================================== -->
1372<div data-pane="domain" class="hide">
1373 <div class="dash-grid">
1374 <div class="module">
1375 <div class="module-head">
1376 <div class="left">
1377 <div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/></svg></div>
1378 <div><div class="module-title">Domains</div><div class="module-sub">Subdomain is permanent · custom domain optional</div></div>
1379 </div>
1380 </div>
1381 <div class="module-body">
1382 [if:$renamed_flash]
1383 <div class="banner success" style="margin-bottom:14px">
1384 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
1385 <div class="text-xs">Store name updated.</div>
1386 </div>
1387 [/if]
1388
1389 <form method="POST" action="/storefront.cgi" style="margin-bottom:18px">
1390 <input type="hidden" name="action" value="rename">
1391 <input type="hidden" name="store_id" value="$store_id">
1392 <div class="form-group">
1393 <label class="form-label">Store name</label>
1394 <div class="flex items-center gap-2">
1395 <input class="input" type="text" name="name" value="$store_name" maxlength="120" style="flex:1">
1396 <button type="submit" class="btn btn-primary btn-sm">Save name</button>
1397 </div>
1398 <div class="form-help">Friendly brand name shown to buyers. Change anytime.</div>
1399 </div>
1400 </form>
1401
1402 <div class="form-group">
1403 <label class="form-label">Subdomain</label>
1404 <div class="flex items-center gap-2">
1405 <input class="input" value="$store_subdomain" readonly style="flex:1;opacity:0.7">
1406 <span class="text-secondary">.webstls.com</span>
1407 </div>
1408 <div class="form-help">Subdomains aren't renameable. To use a different one, delete this storefront and create a new one.</div>
1409 </div>
1410
1411 [if:$domain_saved_flash]
1412 <div class="banner success" style="margin-bottom:14px">
1413 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
1414 <div class="text-xs">Custom domain saved. Point your DNS at our server; SSL is provisioned via Let's Encrypt on first request.</div>
1415 </div>
1416 [/if]
1417 [if:$domain_cleared_flash]
1418 <div class="banner success" style="margin-bottom:14px">
1419 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
1420 <div class="text-xs">Custom domain cleared. Buyers will now reach this store at the subdomain.</div>
1421 </div>
1422 [/if]
1423 [if:$domain_err_invalid]
1424 <div class="banner danger" style="margin-bottom:14px">
1425 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
1426 <div class="text-xs">That does not look like a valid domain. Use a bare hostname like <code>yourstore.com</code> &mdash; no <code>http://</code>, no path.</div>
1427 </div>
1428 [/if]
1429 [if:$domain_err_taken]
1430 <div class="banner danger" style="margin-bottom:14px">
1431 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
1432 <div class="text-xs">That domain is already attached to another storefront on the platform.</div>
1433 </div>
1434 [/if]
1435
1436 <form method="POST" action="/storefront.cgi">
1437 <input type="hidden" name="action" value="set_custom_domain">
1438 <input type="hidden" name="store_id" value="$store_id">
1439 <div class="form-group">
1440 <label class="form-label">Custom domain (optional)</label>
1441 <input class="input" name="custom_domain" value="$store_custom" placeholder="yourstore.com" autocomplete="off">
1442 <div class="form-help">Point an A or CNAME record at our server, then enter the domain here. SSL is provisioned via Let's Encrypt on first hit. Leave blank to clear and fall back to the subdomain.</div>
1443 </div>
1444
1445 [if:$has_custom_domain]
1446 <div class="banner success">
1447 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
1448 <div class="text-xs">SSL: <strong>$ssl_status</strong> for <code>$store_custom</code></div>
1449 </div>
1450 [/if]
1451 [if:$no_custom_domain]
1452 <div class="banner info">
1453 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
1454 <div class="text-xs">No custom domain configured. Buyers can reach your store via the subdomain or <code>/store.cgi?id=$store_id</code>.</div>
1455 </div>
1456 [/if]
1457
1458 <button type="submit" class="btn btn-primary mt-3">Save domain</button>
1459 </form>
1460 </div>
1461 </div>
1462
1463 <div class="module">
1464 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div><div><div class="module-title">Security</div><div class="module-sub">Storefront protections</div></div></div></div>
1465 <div class="module-body" style="display:flex;flex-direction:column;gap:14px">
1466 <div class="flex items-center justify-between"><span class="text-sm fw-600">Force HTTPS</span><span class="toggle on"></span></div>
1467 <div class="flex items-center justify-between"><span class="text-sm fw-600">Bot challenge on checkout</span><span class="toggle on"></span></div>
1468 <div class="flex items-center justify-between"><span class="text-sm fw-600">Buyer 2FA optional</span><span class="toggle"></span></div>
1469 <div class="flex items-center justify-between"><span class="text-sm fw-600">Geo‑block test mode</span><span class="toggle"></span></div>
1470 </div>
1471 </div>
1472 </div>
1473</div>
1474
1475
1476<!-- ===== Features =================================================== -->
1477<div data-pane="features" class="hide">
1478 <div class="module">
1479 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1Z"/><line x1="4" y1="22" x2="4" y2="15"/></svg></div><div><div class="module-title">Storefront features</div><div class="module-sub">Per-store toggles. Turn on what your store needs.</div></div></div></div>
1480 <div class="module-body" style="display:flex;flex-direction:column;gap:10px">
1481 <div class="flex items-center justify-between" style="padding:12px;border:1px solid var(--col-border);border-radius:10px;background:var(--col-surface-2)">
1482 <div><div class="text-sm fw-600">Buyer accounts &amp; saved orders</div><div class="text-xs text-dim">Re-downloads, license history, order tracking</div></div>
1483 <span class="toggle on"></span>
1484 </div>
1485 <div class="flex items-center justify-between" style="padding:12px;border:1px solid var(--col-border);border-radius:10px;background:var(--col-surface-2)">
1486 <div><div class="text-sm fw-600">Reviews &amp; ratings</div><div class="text-xs text-dim">Verified buyers can review purchases</div></div>
1487 <span class="toggle"></span>
1488 </div>
1489 <div class="flex items-center justify-between" style="padding:12px;border:1px solid var(--col-border);border-radius:10px;background:var(--col-surface-2)">
1490 <div><div class="text-sm fw-600">Wishlist</div><div class="text-xs text-dim">Save for later · email when on sale</div></div>
1491 <span class="toggle"></span>
1492 </div>
1493 <div class="flex items-center justify-between" style="padding:12px;border:1px solid var(--col-border);border-radius:10px;background:var(--col-surface-2)">
1494 <div><div class="text-sm fw-600">Free model of the week</div><div class="text-xs text-dim">Lead capture for newsletter list</div></div>
1495 <span class="toggle"></span>
1496 </div>
1497 </div>
1498 </div>
1499</div>
1500
1501
1502<!-- ===== Payouts ==================================================== -->
1503<div data-pane="payouts" class="hide">
1504 [if:$stripe_just_connected]
1505 <div class="banner success mb-3">
1506 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
1507 <div class="text-sm fw-600">Stripe connected. Your storefront can now accept payments.</div>
1508 </div>
1509 [/if]
1510 [if:$stripe_just_pending]
1511 <div class="banner warning mb-3">
1512 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
1513 <div class="text-sm fw-600">Stripe still needs a few details. Click Continue setup to finish.</div>
1514 </div>
1515 [/if]
1516 [if:$has_stripe_error]
1517 <div class="banner danger mb-3">
1518 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
1519 <div class="text-sm fw-600">Stripe error: $stripe_error</div>
1520 </div>
1521 [/if]
1522
1523 <div class="module">
1524 <div class="module-head"><div class="left"><div class="module-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></div><div><div class="module-title">Stripe Connect</div><div class="module-sub">Direct payouts - WebSTLs never holds creator funds</div></div></div></div>
1525 <div class="module-body">
1526 [if:$stripe_is_connected]
1527 <div class="flex items-center justify-between" style="padding:14px;border-radius:10px;background:rgba(34,197,94,0.06);border:1px solid rgba(34,197,94,0.25);margin-bottom:14px">
1528 <div><div class="text-sm fw-600" style="color:#fff">Connected and accepting payments</div><div class="text-xs text-dim">Payouts go directly to your Stripe account. WebSTLs takes a 10% application fee per charge.</div></div>
1529 <span class="pill success">Live</span>
1530 </div>
1531 <a href="https://dashboard.stripe.com/" target="_blank" rel="noopener" class="btn btn-secondary">Open Stripe dashboard</a>
1532 <a href="$stripe_connect_url" class="btn btn-ghost" style="margin-left:8px">Update account details</a>
1533 [/if]
1534
1535 [if:$stripe_is_pending]
1536 <div class="flex items-center justify-between" style="padding:14px;border-radius:10px;background:rgba(234,179,8,0.06);border:1px solid rgba(234,179,8,0.25);margin-bottom:14px">
1537 <div><div class="text-sm fw-600" style="color:#fff">Onboarding in progress</div><div class="text-xs text-dim">Stripe still needs business details, banking info, or identity verification before you can accept payments.</div></div>
1538 <span class="pill warning">Pending</span>
1539 </div>
1540 <a href="$stripe_connect_url" class="btn btn-primary">Continue Stripe setup</a>
1541 [/if]
1542
1543 [if:$stripe_is_not_started]
1544 <div class="banner info">
1545 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/></svg>
1546 <div class="text-xs">Connect a Stripe account so buyers can check out. Stripe takes payment processing fees; WebSTLs adds a 10% application fee on top. No funds ever sit with WebSTLs.</div>
1547 </div>
1548 <a href="$stripe_connect_url" class="btn btn-primary mt-3">Connect Stripe</a>
1549 [/if]
1550 </div>
1551 </div>
1552</div>
1553
1554<!-- =============================================================
1555 Classy confirm modal. Any element with data-confirm-title /
1556 data-confirm-body / data-confirm-ok (and optional data-confirm-
1557 tone="danger") gets a centered overlay instead of the browser's
1558 ugly native confirm() dialog. JS hijacks the click (or the
1559 parent form's submit), shows the modal, and only fires the
1560 real action when the user clicks the confirm button.
1561============================================================= -->
1562<style>
1563 /* Base state: completely inert. No backdrop-filter, no background,
1564 no animation, no positioning -- so even if a browser tries to
1565 promote a compositing layer for a hidden element, there's nothing
1566 to render. Everything visual lives on .is-open. */
1567 .cf-overlay { display:none; }
1568 .cf-overlay.is-open {
1569 position:fixed; inset:0; z-index:9999;
1570 background:rgba(2,6,18,0.72);
1571 backdrop-filter:blur(8px); -webkit-backdrop-filter:blur(8px);
1572 display:flex;
1573 align-items:center; justify-content:center;
1574 padding:20px;
1575 overflow-y:auto;
1576 animation:cf-fade-in 0.18s ease-out;
1577 }
1578 @keyframes cf-fade-in { from { opacity:0 } to { opacity:1 } }
1579
1580 /* Card starts already visible -- earlier version started at opacity:0
1581 and animated via @keyframes cf-pop, but Perl qq~~ in MODS::Template
1582 reads `@keyframes` as an array variable and strips it. The animation
1583 never ran, so the card was permanently invisible -- only the dark
1584 backdrop showed and the page looked mysteriously "dimmed". A plain
1585 CSS transition on the .is-open class gives us the pop without the
1586 stripped at-rule. */
1587 .cf-card {
1588 width:100%; max-width:480px;
1589 background:linear-gradient(180deg, var(--col-surface-1) 0%, var(--col-bg-2) 100%);
1590 border:1px solid var(--col-border);
1591 border-radius:16px;
1592 box-shadow:0 30px 90px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04) inset;
1593 padding:28px 30px 24px;
1594 transform:translateY(8px); opacity:0;
1595 transition:transform 0.22s cubic-bezier(0.34,1.56,0.64,1), opacity 0.18s ease-out;
1596 }
1597 .cf-overlay.is-open .cf-card { transform:translateY(0); opacity:1; }
1598 .cf-icon {
1599 width:54px; height:54px; border-radius:14px;
1600 display:grid; place-items:center; margin:0 0 16px;
1601 background:linear-gradient(135deg, rgba(59,130,246,0.20), rgba(99,102,241,0.16));
1602 border:1px solid rgba(59,130,246,0.40);
1603 color:#93c5fd;
1604 }
1605 .cf-card.is-danger .cf-icon {
1606 background:linear-gradient(135deg, rgba(239,68,68,0.22), rgba(220,38,38,0.16));
1607 border-color:rgba(239,68,68,0.50);
1608 color:#fca5a5;
1609 }
1610 .cf-title {
1611 font-family:var(--font-display, inherit);
1612 font-size:20px; font-weight:700; color:#fff;
1613 margin:0 0 8px; letter-spacing:0.2px;
1614 }
1615 .cf-body {
1616 font-size:13.5px; line-height:1.65; color:var(--col-text-2);
1617 margin:0 0 22px;
1618 }
1619 .cf-actions {
1620 display:flex; gap:10px; justify-content:flex-end; flex-wrap:wrap;
1621 }
1622 .cf-btn {
1623 padding:10px 18px; border-radius:10px;
1624 font-size:13.5px; font-weight:700; letter-spacing:0.2px;
1625 cursor:pointer; border:1px solid var(--col-border);
1626 background:var(--col-surface-2); color:var(--col-text);
1627 transition:background 0.15s, border-color 0.15s, transform 0.12s, color 0.15s, box-shadow 0.15s;
1628 }
1629 .cf-btn:hover { background:var(--col-surface-3); transform:translateY(-1px); }
1630 .cf-btn-primary {
1631 background:linear-gradient(135deg, #3b82f6, #6366f1);
1632 border-color:rgba(59,130,246,0.70);
1633 color:#fff;
1634 box-shadow:0 6px 18px rgba(59,130,246,0.42);
1635 }
1636 .cf-btn-primary:hover { filter:brightness(1.08); box-shadow:0 8px 22px rgba(59,130,246,0.55); }
1637 .cf-btn-danger {
1638 background:linear-gradient(135deg, #ef4444, #dc2626);
1639 border-color:rgba(239,68,68,0.75);
1640 color:#fff;
1641 box-shadow:0 6px 18px rgba(239,68,68,0.42);
1642 }
1643 .cf-btn-danger:hover { filter:brightness(1.08); box-shadow:0 8px 22px rgba(239,68,68,0.55); }
1644</style>
1645
1646<div class="cf-overlay" id="cf-overlay" role="dialog" aria-modal="true" aria-labelledby="cf-title" hidden style="display:none">
1647 <div class="cf-card" id="cf-card">
1648 <div class="cf-icon" id="cf-icon">
1649 <svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
1650 </div>
1651 <h3 class="cf-title" id="cf-title">Are you sure?</h3>
1652 <p class="cf-body" id="cf-body">This action will be applied.</p>
1653 <div class="cf-actions">
1654 <button type="button" class="cf-btn" id="cf-cancel">Cancel</button>
1655 <button type="button" class="cf-btn cf-btn-primary" id="cf-ok">Confirm</button>
1656 </div>
1657 </div>
1658</div>
1659
1660<script>
1661(function () {
1662 var overlay = document.getElementById('cf-overlay');
1663 var card = document.getElementById('cf-card');
1664 var titleEl = document.getElementById('cf-title');
1665 var bodyEl = document.getElementById('cf-body');
1666 var okBtn = document.getElementById('cf-ok');
1667 var cancel = document.getElementById('cf-cancel');
1668 if (!overlay) return;
1669
1670 var pending = null; // { run: function, original: Event }
1671
1672 function open(opts) {
1673 titleEl.textContent = opts.title || 'Are you sure?';
1674 bodyEl.textContent = opts.body || 'This action will be applied.';
1675 okBtn.textContent = opts.ok || 'Confirm';
1676 if (opts.tone === 'danger') {
1677 card.classList.add('is-danger');
1678 okBtn.classList.remove('cf-btn-primary');
1679 okBtn.classList.add('cf-btn-danger');
1680 } else {
1681 card.classList.remove('is-danger');
1682 okBtn.classList.add('cf-btn-primary');
1683 okBtn.classList.remove('cf-btn-danger');
1684 }
1685 overlay.removeAttribute('hidden');
1686 overlay.style.display = 'flex';
1687 overlay.classList.add('is-open');
1688 setTimeout(function(){ okBtn.focus(); }, 60);
1689 }
1690 function close() {
1691 overlay.classList.remove('is-open');
1692 overlay.style.display = 'none';
1693 overlay.setAttribute('hidden', '');
1694 pending = null;
1695 }
1696
1697 okBtn.addEventListener('click', function () {
1698 var run = pending && pending.run;
1699 close();
1700 if (typeof run === 'function') run();
1701 });
1702 cancel.addEventListener('click', close);
1703 overlay.addEventListener('click', function (e) {
1704 if (e.target === overlay) close();
1705 });
1706 document.addEventListener('keydown', function (e) {
1707 if (e.key === 'Escape' && overlay.classList.contains('is-open')) close();
1708 });
1709
1710 // Find the nearest ancestor (or the element itself) that carries
1711 // data-confirm-title -- that's the source of the prompt copy.
1712 function confirmSource(el) {
1713 while (el && el !== document.body) {
1714 if (el.hasAttribute && el.hasAttribute('data-confirm-title')) return el;
1715 el = el.parentElement;
1716 }
1717 return null;
1718 }
1719 function opts(el) {
1720 return {
1721 title: el.getAttribute('data-confirm-title') || '',
1722 body: el.getAttribute('data-confirm-body') || '',
1723 ok: el.getAttribute('data-confirm-ok') || 'Confirm',
1724 tone: el.getAttribute('data-confirm-tone') || 'default',
1725 };
1726 }
1727
1728 // Intercept form submits where the form (or a clicked submit
1729 // button inside it) declares data-confirm-title.
1730 document.querySelectorAll('form').forEach(function (form) {
1731 form.addEventListener('submit', function (e) {
1732 // Already confirmed by the OK click below -- let the submit go.
1733 if (form.dataset.cfPassed === '1') { form.dataset.cfPassed = ''; return; }
1734 var src = confirmSource(form) || confirmSource(document.activeElement);
1735 if (!src) return;
1736 e.preventDefault();
1737 pending = {
1738 run: function () {
1739 form.dataset.cfPassed = '1';
1740 form.submit();
1741 }
1742 };
1743 open(opts(src));
1744 });
1745 });
1746
1747 // Intercept clicks on standalone buttons / links that carry
1748 // data-confirm-title but aren't inside a form (or whose form
1749 // we want to short-circuit before submit).
1750 document.querySelectorAll('[data-confirm-title]').forEach(function (el) {
1751 if (el.tagName === 'FORM') return; // handled above
1752 el.addEventListener('click', function (e) {
1753 if (el.dataset.cfPassed === '1') { el.dataset.cfPassed = ''; return; }
1754 // If it's a submit button, let the form submit handler above
1755 // catch it -- avoid double-prompting.
1756 if (el.type === 'submit' && el.form) return;
1757 e.preventDefault();
1758 pending = {
1759 run: function () {
1760 el.dataset.cfPassed = '1';
1761 if (el.tagName === 'A' && el.href) { location.href = el.href; }
1762 else { el.click(); }
1763 }
1764 };
1765 open(opts(el));
1766 });
1767 });
1768})();
1769</script>
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help