PageSourceSearch

https://laserlab.app/qrcode

html laserlab.app collected 2026-09-24 14:31:25 UTC 81,617 bytes, 2,674 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>LaserLab - QR Code Generator</title>
7    
8    <!-- Basic Meta -->
9    <meta name="description" content="Your laser's best friend. Image prep, slicer for collages, ruler gen, QR code gen - everything to get your engrave on.">
10    <meta name="author" content="LaserLab">
11    
12    <!-- Open Graph (Facebook, LinkedIn, etc.) -->
13    <meta property="og:type" content="website">
14    <meta property="og:url" content="https://laserlab.app/">
15    <meta property="og:title" content="LaserLab - Your Laser's Best Friend">
16    <meta property="og:description" content="Your laser's best friend. Image prep, slicer for collages, ruler gen, QR code gen - everything to get your engrave on.">
17    <meta property="og:image" content="https://laserlab.app/static/images/og-image.jpg">
18    <meta property="og:image:width" content="1200">
19    <meta property="og:image:height" content="630">
20    <meta property="og:site_name" content="LaserLab">
21    
22    <!-- Twitter Card -->
23    <meta name="twitter:card" content="summary_large_image">
24    <meta name="twitter:title" content="LaserLab - Your Laser's Best Friend">
25    <meta name="twitter:description" content="Your laser's best friend. Image prep, slicer for collages, ruler gen, QR code gen - everything to get your engrave on.">
26    <meta name="twitter:image" content="https://laserlab.app/static/images/og-image.jpg">
27    
28    <!-- Preconnect to CDNs for faster loading -->
29    <link rel="preconnect" href="https://cdnjs.cloudflare.com">
30    <link rel="preconnect" href="https://cdn.jsdelivr.net">
31    <link rel="preconnect" href="https://assets.laserlab.app">
32    <link rel="preconnect" href="https://www.gstatic.com">
33    <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com">
34    <link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
35    <link rel="dns-prefetch" href="https://assets.laserlab.app">
36    
37    <!-- Favicon -->
38    <link rel="icon" type="image/svg+xml" href="/static/images/favicon.svg?v=1775399980">
39    <link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32.png?v=1775399979">
40    <link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16.png?v=1775399979">
41    <link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png?v=1775399979">
42    <link rel="shortcut icon" href="/static/images/favicon.ico?v=1775399979">
43    
44    <!-- Custom CSS (load early) -->
45    <link rel="stylesheet" href="/static/css/style.css?v=1775399974?v=1786132325">
46    
47    <!-- noUiSlider CSS -->
48    <link rel="stylesheet" href="https://assets.laserlab.app/nouislider.min.css">
49    
50    
51<style>
52/* QR CODE GENERATOR v2025-01-20-VCARD-NOTE */
53* {
54    box-sizing: border-box;
55}
56
57body {
58    margin: 0;
59    padding: 0;
60    background: #0f172a;
61    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
62    overflow: hidden;
63}
64
65/* Faded homepage in background */
66.homepage-bg {
67    position: fixed;
68    top: 0;
69    left: 0;
70    width: 100%;
71    height: 100%;
72    opacity: 0.25;
73    pointer-events: none;
74    overflow: hidden;
75}
76
77.homepage-bg .home-header {
78    text-align: center;
79    padding: 5px 20px 5px;
80}
81
82.homepage-bg .home-logo {
83    max-width: 380px;
84    height: auto;
85    margin-top: -40px;
86    margin-bottom: -50px;
87}
88
89.homepage-bg .home-tagline {
90    color: #94a3b8;
91    font-size: 1.1rem;
92    margin: 0 0 5px;
93    font-weight: 300;
94}
95
96.homepage-bg .home-features {
97    display: flex;
98    justify-content: center;
99    gap: 25px;
100    padding: 10px 30px;
101    flex: 1;
102    align-items: flex-start;
103    flex-wrap: wrap;
104}
105
106.homepage-bg .feature-box {
107    width: 380px;
108    background: rgba(20, 30, 50, 0.8);
109    border: 2px solid #3b82f6;
110    border-radius: 8px;
111    overflow: hidden;
112    display: flex;
113    flex-direction: column;
114    box-shadow: 0 0 20px rgba(59, 130, 246, 0.3);
115}
116
117.homepage-bg .feature-box.green {
118    border-color: #22c55e;
119    box-shadow: 0 0 20px rgba(34, 197, 94, 0.3);
120}
121
122.homepage-bg .feature-box.cyan {
123    border-color: #06b6d4;
124    box-shadow: 0 0 20px rgba(6, 182, 212, 0.3);
125}
126
127.homepage-bg .feature-box.purple {
128    border-color: #8b5cf6;
129    box-shadow: 0 0 20px rgba(139, 92, 246, 0.3);
130}
131
132.homepage-bg .feature-box.mini {
133    width: 300px;
134    flex-direction: row;
135    height: auto;
136}
137
138.homepage-bg .feature-box.mini .feature-img {
139    width: 120px;
140    height: 120px;
141    object-fit: cover;
142    flex-shrink: 0;
143}
144
145.homepage-bg .feature-box.mini .feature-info {
146    padding: 12px 15px;
147}
148
149.homepage-bg .feature-img {
150    width: 100%;
151    height: 240px;
152    object-fit: cover;
153}
154
155.homepage-bg .feature-info {
156    padding: 15px 20px;
157}
158
159.homepage-bg .feature-name {
160    color: #fff;
161    font-size: 1.4rem;
162    font-weight: 600;
163    margin: 0 0 8px;
164}
165
166.homepage-bg .feature-subtitle {
167    color: #22c55e;
168    font-size: 0.8rem;
169    font-weight: 500;
170    margin: -4px 0 6px;
171}
172
173.homepage-bg .feature-desc {
174    color: #94a3b8;
175    font-size: 0.9rem;
176    line-height: 1.5;
177    margin: 0;
178}
179
180.homepage-bg .mini-tools-column {
181    display: flex;
182    flex-direction: column;
183    gap: 20px;
184}
185
186/* Backdrop with slight darkening */
187.qr-backdrop {
188    position: fixed;
189    top: 0;
190    left: 0;
191    width: 100%;
192    height: 100%;
193    background: rgba(15, 23, 42, 0.7);
194    display: flex;
195    align-items: center;
196    justify-content: center;
197    z-index: 100;
198}
199
200/* Main popup container - LARGER */
201.qr-popup {
202    background: #1e293b;
203    border: 1px solid #8b5cf6;
204    border-radius: 16px;
205    box-shadow: 0 0 60px rgba(139, 92, 246, 0.3);
206    width: 1060px;
207    max-width: 95vw;
208    max-height: 90vh;
209    overflow: hidden;
210    display: flex;
211    flex-direction: column;
212}
213
214/* Header */
215.qr-header {
216    display: flex;
217    justify-content: space-between;
218    align-items: center;
219    padding: 16px 24px;
220    background: rgba(15, 23, 42, 0.8);
221    border-bottom: 1px solid #334155;
222}
223
224.qr-title {
225    color: #8b5cf6;
226    font-size: 18px;
227    font-weight: 600;
228    display: flex;
229    align-items: center;
230    gap: 8px;
231}
232
233.qr-close {
234    background: transparent;
235    border: none;
236    color: #94a3b8;
237    font-size: 24px;
238    cursor: pointer;
239    padding: 4px 8px;
240    border-radius: 4px;
241    transition: all 0.2s;
242    text-decoration: none;
243}
244
245.qr-close:hover {
246    background: rgba(239, 68, 68, 0.2);
247    color: #ef4444;
248}
249
250/* Tab Bar Wrapper */
251.qr-tabs-wrapper {
252    position: relative;
253    background: rgba(15, 23, 42, 0.5);
254    border-bottom: 1px solid #334155;
255}
256
257/* Scroll hint - hidden on desktop */
258.tabs-scroll-hint {
259    display: none;
260}
261
262/* Tab Bar - ImagePrep style */
263.qr-tabs {
264    display: flex;
265    gap: 8px;
266    padding: 16px 24px;
267    justify-content: center;
268}
269
270.qr-tab {
271    padding: 12px 28px;
272    font-size: 13px;
273    font-weight: 600;
274    letter-spacing: 1px;
275    text-transform: uppercase;
276    border: 1px solid rgba(148, 163, 184, 0.2);
277    background: rgba(30, 41, 59, 0.6);
278    color: #cbd5e1;
279    border-radius: 6px;
280    cursor: pointer;
281    transition: all 0.3s;
282    display: flex;
283    align-items: center;
284    gap: 8px;
285}
286
287.qr-tab:hover {
288    border-color: #8b5cf6;
289    color: #ffffff;
290    background: rgba(139, 92, 246, 0.1);
291}
292
293.qr-tab.active {
294    background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
295    border-color: transparent;
296    color: #ffffff;
297    box-shadow: 0 0 20px rgba(139, 92, 246, 0.4);
298}
299
300.tab-stacked {
301    display: block;
302    text-align: center;
303    line-height: 1.2;
304    font-size: 12px;
305}
306
307/* Body - Two Column Layout */
308.qr-body {
309    display: flex;
310    padding: 24px;
311    gap: 24px;
312    overflow: hidden;
313    flex: 1;
314    min-height: 0;
315}
316
317/* Left Column - Controls (scrollable) */
318.qr-controls {
319    width: 280px;
320    flex-shrink: 0;
321    display: flex;
322    flex-direction: column;
323    gap: 16px;
324    overflow-y: auto;
325    padding-right: 8px;
326}
327
328/* Scrollbar styling for controls */
329.qr-controls::-webkit-scrollbar {
330    width: 6px;
331}
332
333.qr-controls::-webkit-scrollbar-track {
334    background: rgba(15, 23, 42, 0.5);
335    border-radius: 3px;
336}
337
338.qr-controls::-webkit-scrollbar-thumb {
339    background: rgba(139, 92, 246, 0.4);
340    border-radius: 3px;
341}
342
343.qr-controls::-webkit-scrollbar-thumb:hover {
344    background: rgba(139, 92, 246, 0.6);
345}
346
347/* Tab Content Panels */
348.tab-content {
349    display: none;
350}
351
352.tab-content.active {
353    display: block;
354}
355
356/* Control Groups */
357.control-group {
358    background: rgba(30, 41, 59, 0.5);
359    border: 1px solid #334155;
360    border-radius: 10px;
361    padding: 14px;
362}
363
364.control-label {
365    color: #bae6fd;
366    font-size: 11px;
367    font-weight: 600;
368    text-transform: uppercase;
369    letter-spacing: 0.5px;
370    margin-bottom: 10px;
371    display: flex;
372    align-items: center;
373    gap: 6px;
374}
375
376.control-label .label-icon {
377    font-size: 13px;
378}
379
380/* Input Styles */
381.qr-input {
382    width: 100%;
383    padding: 10px 12px;
384    background: #1e293b;
385    border: 1px solid #475569;
386    border-radius: 6px;
387    color: #e2e8f0;
388    font-size: 13px;
389    transition: border-color 0.2s;
390}
391
392.qr-input:focus {
393    outline: none;
394    border-color: #8b5cf6;
395}
396
397.qr-input::placeholder {
398    color: #64748b;
399}
400
401textarea.qr-input {
402    resize: vertical;
403    min-height: 80px;
404    font-family: inherit;
405}
406
407.input-row {
408    display: flex;
409    align-items: center;
410    gap: 8px;
411}
412
413.input-row .qr-input {
414    flex: 1;
415}
416
417.input-suffix {
418    color: #64748b;
419    font-size: 13px;
420    font-weight: 500;
421}
422
423/* Section Divider */
424.section-divider {
425    height: 1px;
426    background: #334155;
427    margin: 4px 0;
428}
429
430/* Style Section Header */
431.style-header {
432    color: #64748b;
433    font-size: 10px;
434    font-weight: 600;
435    text-transform: uppercase;
436    letter-spacing: 1px;
437    margin-bottom: 12px;
438    padding-bottom: 8px;
439    border-bottom: 1px solid #334155;
440}
441
442/* Shape Buttons */
443.shape-btns {
444    display: flex;
445    gap: 8px;
446}
447
448.shape-btn {
449    flex: 1;
450    padding: 10px 8px;
451    background: transparent;
452    border: 1px solid #475569;
453    color: #94a3b8;
454    border-radius: 8px;
455    cursor: pointer;
456    font-size: 12px;
457    font-weight: 500;
458    transition: all 0.2s;
459    display: flex;
460    flex-direction: column;
461    align-items: center;
462    gap: 4px;
463}
464
465.shape-btn .shape-icon {
466    font-size: 18px;
467}
468
469.shape-btn.active {
470    background: rgba(139, 92, 246, 0.15);
471    border-color: #8b5cf6;
472    color: #c4b5fd;
473}
474
475.shape-btn:hover:not(.active) {
476    border-color: #8b5cf6;
477    color: #e2e8f0;
478}
479
480/* Format Toggle */
481.format-toggle {
482    display: flex;
483    background: #0f172a;
484    border-radius: 6px;
485    padding: 3px;
486}
487
488.format-btn {
489    flex: 1;
490    padding: 8px 12px;
491    background: transparent;
492    border: none;
493    color: #64748b;
494    border-radius: 4px;
495    cursor: pointer;
496    font-size: 12px;
497    font-weight: 600;
498    transition: all 0.2s;
499}
500
501.format-btn.active {
502    background: #8b5cf6;
503    color: white;
504}
505
506.format-btn:hover:not(.active) {
507    color: #e2e8f0;
508}
509
510/* Password Input with Toggle */
511.password-input-wrapper {
512    position: relative;
513    display: flex;
514    align-items: center;
515}
516
517.password-input-wrapper .qr-input {
518    padding-right: 40px;
519}
520
521.password-toggle {
522    position: absolute;
523    right: 8px;
524    background: none;
525    border: none;
526    cursor: pointer;
527    padding: 4px;
528    opacity: 0.6;
529    transition: opacity 0.2s;
530}
531
532.password-toggle:hover {
533    opacity: 1;
534}
535
536.password-toggle .eye-icon {
537    font-size: 16px;
538}
539
540/* Security Type Buttons */
541.security-btns {
542    display: flex;
543    gap: 6px;
544}
545
546.security-btn {
547    flex: 1;
548    padding: 10px 8px;
549    background: transparent;
550    border: 1px solid #475569;
551    color: #94a3b8;
552    border-radius: 6px;
553    cursor: pointer;
554    font-size: 11px;
555    font-weight: 600;
556    transition: all 0.2s;
557}
558
559.security-btn.active {
560    background: rgba(139, 92, 246, 0.15);
561    border-color: #8b5cf6;
562    color: #c4b5fd;
563}
564
565.security-btn:hover:not(.active) {
566    border-color: #8b5cf6;
567    color: #e2e8f0;
568}
569
570/* Display As Buttons (vCard) */
571.display-as-btns {
572    display: flex;
573    gap: 6px;
574}
575
576.display-as-btn {
577    flex: 1;
578    padding: 10px 12px;
579    background: transparent;
580    border: 1px solid #475569;
581    color: #94a3b8;
582    border-radius: 6px;
583    cursor: pointer;
584    font-size: 12px;
585    font-weight: 600;
586    transition: all 0.2s;
587}
588
589.display-as-btn.active {
590    background: rgba(139, 92, 246, 0.15);
591    border-color: #8b5cf6;
592    color: #c4b5fd;
593}
594
595.display-as-btn:hover:not(.active) {
596    border-color: #8b5cf6;
597    color: #e2e8f0;
598}
599
600/* vCard fields */
601.vcard-fields-scroll {
602    max-height: 320px;
603    overflow-y: auto;
604    padding-right: 4px;
605}
606
607.vcard-fields-scroll::-webkit-scrollbar {
608    width: 6px;
609}
610
611.vcard-fields-scroll::-webkit-scrollbar-track {
612    background: #1e293b;
613    border-radius: 3px;
614}
615
616.vcard-fields-scroll::-webkit-scrollbar-thumb {
617    background: #475569;
618    border-radius: 3px;
619}
620
621.vcard-row {
622    display: flex;
623    gap: 8px;
624}
625
626.vcard-row .qr-input {
627    flex: 1;
628}
629
630/* Checkbox styling */
631.checkbox-row {
632    display: flex;
633    align-items: center;
634    justify-content: space-between;
635}
636
637.checkbox-label {
638    color: #94a3b8;
639    font-size: 13px;
640}
641
642.checkbox-hint {
643    color: #64748b;
644    font-size: 10px;
645    margin-top: 6px;
646}
647
648.toggle-switch {
649    position: relative;
650    width: 44px;
651    height: 24px;
652}
653
654.toggle-switch input {
655    opacity: 0;
656    width: 0;
657    height: 0;
658}
659
660.toggle-slider {
661    position: absolute;
662    cursor: pointer;
663    top: 0;
664    left: 0;
665    right: 0;
666    bottom: 0;
667    background-color: #475569;
668    transition: 0.3s;
669    border-radius: 24px;
670}
671
672.toggle-slider:before {
673    position: absolute;
674    content: "";
675    height: 18px;
676    width: 18px;
677    left: 3px;
678    bottom: 3px;
679    background-color: white;
680    transition: 0.3s;
681    border-radius: 50%;
682}
683
684input:checked + .toggle-slider {
685    background-color: #8b5cf6;
686}
687
688input:checked + .toggle-slider:before {
689    transform: translateX(20px);
690}
691
692/* Help Icon */
693.help-icon {
694    display: inline-flex;
695    align-items: center;
696    justify-content: center;
697    width: 16px;
698    height: 16px;
699    background: #475569;
700    border: none;
701    border-radius: 50%;
702    color: #e2e8f0;
703    font-size: 10px;
704    font-weight: 700;
705    cursor: pointer;
706    margin-left: 6px;
707    transition: all 0.2s;
708    font-family: inherit;
709}
710
711.help-icon:hover {
712    background: #8b5cf6;
713    transform: scale(1.1);
714}
715
716/* Input hint text */
717.input-hint {
718    color: #64748b;
719    font-size: 10px;
720    margin-top: 6px;
721}
722
723/* Review URL preview */
724.review-preview-url {
725    margin-top: 12px;
726    padding: 10px 12px;
727    background: rgba(15, 23, 42, 0.5);
728    border: 1px solid #334155;
729    border-radius: 6px;
730    color: #64748b;
731    font-size: 11px;
732    word-break: break-all;
733    line-height: 1.4;
734}
735
736.review-preview-url.valid {
737    color: #22c55e;
738    border-color: rgba(34, 197, 94, 0.3);
739    background: rgba(34, 197, 94, 0.1);
740}
741
742/* Review Info Link */
743.review-info-link {
744    display: flex;
745    align-items: center;
746    gap: 6px;
747    padding: 10px 12px;
748    margin-bottom: 16px;
749    background: rgba(139, 92, 246, 0.1);
750    border: 1px solid rgba(139, 92, 246, 0.3);
751    border-radius: 8px;
752    color: #a78bfa;
753    font-size: 13px;
754    cursor: pointer;
755    transition: all 0.2s ease;
756}
757
758.review-info-link:hover {
759    background: rgba(139, 92, 246, 0.2);
760    border-color: rgba(139, 92, 246, 0.5);
761}
762
763.review-info-link .info-link-action {
764    color: #8b5cf6;
765    font-weight: 500;
766}
767
768/* Place ID Help Modal */
769.placeid-help-modal {
770    display: none;
771    position: fixed;
772    top: 0;
773    left: 0;
774    width: 100%;
775    height: 100%;
776    background: rgba(0, 0, 0, 0.85);
777    z-index: 3000;
778    justify-content: center;
779    align-items: center;
780    padding: 20px;
781}
782
783.placeid-help-content {
784    background: #1e293b;
785    border: 1px solid #8b5cf6;
786    border-radius: 12px;
787    max-width: 520px;
788    width: 100%;
789    max-height: 85vh;
790    overflow-y: auto;
791    box-shadow: 0 0 40px rgba(139, 92, 246, 0.3);
792}
793
794.placeid-help-header {
795    display: flex;
796    justify-content: space-between;
797    align-items: center;
798    padding: 16px 20px;
799    border-bottom: 1px solid #334155;
800    background: rgba(15, 23, 42, 0.5);
801}
802
803.placeid-help-title {
804    color: #8b5cf6;
805    font-size: 16px;
806    font-weight: 600;
807    display: flex;
808    align-items: center;
809    gap: 8px;
810}
811
812.placeid-help-close {
813    background: transparent;
814    border: none;
815    color: #94a3b8;
816    font-size: 20px;
817    cursor: pointer;
818    padding: 4px 8px;
819    border-radius: 4px;
820    transition: all 0.2s;
821}
822
823.placeid-help-close:hover {
824    background: rgba(239, 68, 68, 0.2);
825    color: #ef4444;
826}
827
828.placeid-help-body {
829    padding: 20px;
830}
831
832.placeid-step {
833    display: flex;
834    align-items: flex-start;
835    gap: 12px;
836    margin-bottom: 16px;
837    padding-bottom: 16px;
838    border-bottom: 1px solid #334155;
839}
840
841.placeid-step:last-of-type {
842    margin-bottom: 0;
843    padding-bottom: 0;
844    border-bottom: none;
845}
846
847.placeid-step-num {
848    background: #8b5cf6;
849    color: white;
850    width: 26px;
851    height: 26px;
852    border-radius: 50%;
853    display: flex;
854    align-items: center;
855    justify-content: center;
856    font-size: 13px;
857    font-weight: bold;
858    flex-shrink: 0;
859}
860
861.placeid-step-content {
862    flex: 1;
863}
864
865.placeid-step-content h4 {
866    color: #e2e8f0;
867    font-size: 13px;
868    font-weight: 600;
869    margin: 0 0 6px;
870}
871
872.placeid-step-content p {
873    color: #94a3b8;
874    font-size: 12px;
875    line-height: 1.5;
876    margin: 0;
877}
878
879.placeid-step-content a {
880    color: #8b5cf6;
881    text-decoration: none;
882}
883
884.placeid-step-content a:hover {
885    text-decoration: underline;
886}
887
888.placeid-url-link {
889    display: block;
890    margin-top: 8px;
891    padding: 10px 14px;
892    background: rgba(139, 92, 246, 0.15);
893    border: 1px solid rgba(139, 92, 246, 0.3);
894    border-radius: 6px;
895    color: #c4b5fd !important;
896    font-size: 12px;
897    font-family: monospace;
898    word-break: break-all;
899    transition: all 0.2s;
900}
901
902.placeid-url-link:hover {
903    background: rgba(139, 92, 246, 0.25);
904    border-color: #8b5cf6;
905    text-decoration: none !important;
906}
907
908.placeid-step-content code {
909    background: rgba(139, 92, 246, 0.2);
910    color: #c4b5fd;
911    padding: 2px 6px;
912    border-radius: 3px;
913    font-size: 11px;
914    font-family: monospace;
915}
916
917.placeid-example {
918    margin-top: 16px;
919    padding: 14px;
920    background: rgba(34, 197, 94, 0.1);
921    border: 1px solid rgba(34, 197, 94, 0.3);
922    border-radius: 8px;
923}
924
925.placeid-example-title {
926    color: #22c55e;
927    font-size: 12px;
928    font-weight: 600;
929    margin-bottom: 8px;
930    display: flex;
931    align-items: center;
932    gap: 6px;
933}
934
935.placeid-example-text {
936    color: #e2e8f0;
937    font-size: 12px;
938    font-family: monospace;
939    word-break: break-all;
940}
941
942.placeid-btn-row {
943    margin-top: 16px;
944    display: flex;
945    gap: 10px;
946}
947
948.placeid-open-btn {
949    flex: 1;
950    padding: 12px;
951    background: linear-gradient(135deg, #8b5cf6, #6366f1);
952    border: none;
953    border-radius: 8px;
954    color: white;
955    font-size: 13px;
956    font-weight: 600;
957    cursor: pointer;
958    transition: all 0.2s;
959    text-decoration: none;
960    text-align: center;
961    display: flex;
962    align-items: center;
963    justify-content: center;
964    gap: 6px;
965}
966
967.placeid-open-btn:hover {
968    transform: translateY(-1px);
969    box-shadow: 0 4px 15px rgba(139, 92, 246, 0.4);
970}
971
972.placeid-close-btn {
973    padding: 12px 20px;
974    background: transparent;
975    border: 1px solid #475569;
976    border-radius: 8px;
977    color: #94a3b8;
978    font-size: 13px;
979    font-weight: 500;
980    cursor: pointer;
981    transition: all 0.2s;
982}
983
984.placeid-close-btn:hover {
985    border-color: #8b5cf6;
986    color: #e2e8f0;
987}
988
989/* Google Places Info Modal additions */
990.google-places-intro {
991    background: rgba(59, 130, 246, 0.1);
992    border: 1px solid rgba(59, 130, 246, 0.3);
993    border-radius: 8px;
994    padding: 12px;
995    margin-bottom: 16px;
996    color: #e2e8f0;
997    font-size: 13px;
998    line-height: 1.5;
999}
1000
1001.google-places-note {
1002    background: rgba(139, 92, 246, 0.1);
1003    border-radius: 8px;
1004    padding: 12px;
1005    margin-top: 16px;
1006    color: #c4b5fd;
1007    font-size: 12px;
1008    line-height: 1.5;
1009}
1010
1011.help-icon-inline {
1012    display: inline-flex;
1013    align-items: center;
1014    justify-content: center;
1015    width: 16px;
1016    height: 16px;
1017    background: #475569;
1018    color: #94a3b8;
1019    border-radius: 50%;
1020    font-size: 10px;
1021    font-weight: bold;
1022    vertical-align: middle;
1023}
1024
1025/* Security Help Modal */
1026.security-help-modal {
1027    display: none;
1028    position: fixed;
1029    top: 0;
1030    left: 0;
1031    width: 100%;
1032    height: 100%;
1033    background: rgba(0, 0, 0, 0.85);
1034    z-index: 3000;
1035    justify-content: center;
1036    align-items: center;
1037    padding: 20px;
1038}
1039
1040.security-help-content {
1041    background: #1e293b;
1042    border: 1px solid #8b5cf6;
1043    border-radius: 12px;
1044    max-width: 480px;
1045    width: 100%;
1046    max-height: 85vh;
1047    overflow-y: auto;
1048    box-shadow: 0 0 40px rgba(139, 92, 246, 0.3);
1049}
1050
1051.security-help-header {
1052    display: flex;
1053    justify-content: space-between;
1054    align-items: center;
1055    padding: 16px 20px;
1056    border-bottom: 1px solid #334155;
1057    position: sticky;
1058    top: 0;
1059    background: #1e293b;
1060}
1061
1062.security-help-title {
1063    color: #8b5cf6;
1064    font-size: 16px;
1065    font-weight: 600;
1066    display: flex;
1067    align-items: center;
1068    gap: 8px;
1069}
1070
1071.security-help-close {
1072    background: transparent;
1073    border: none;
1074    color: #94a3b8;
1075    font-size: 20px;
1076    cursor: pointer;
1077    padding: 4px 8px;
1078    border-radius: 4px;
1079    transition: all 0.2s;
1080}
1081
1082.security-help-close:hover {
1083    background: rgba(239, 68, 68, 0.2);
1084    color: #ef4444;
1085}
1086
1087.security-help-body {
1088    padding: 20px;
1089}
1090
1091.help-section {
1092    margin-bottom: 20px;
1093}
1094
1095.help-section:last-child {
1096    margin-bottom: 0;
1097}
1098
1099.help-section-title {
1100    color: #e2e8f0;
1101    font-size: 14px;
1102    font-weight: 600;
1103    margin-bottom: 10px;
1104    display: flex;
1105    align-items: center;
1106    gap: 8px;
1107}
1108
1109.help-section-title .platform-icon {
1110    font-size: 18px;
1111}
1112
1113.help-steps {
1114    background: rgba(15, 23, 42, 0.5);
1115    border-radius: 8px;
1116    padding: 12px 14px;
1117}
1118
1119.help-steps ol {
1120    margin: 0;
1121    padding-left: 20px;
1122    color: #94a3b8;
1123    font-size: 12px;
1124    line-height: 1.7;
1125}
1126
1127.help-steps li {
1128    margin-bottom: 4px;
1129}
1130
1131.help-steps li:last-child {
1132    margin-bottom: 0;
1133}
1134
1135.help-steps code {
1136    background: rgba(139, 92, 246, 0.2);
1137    color: #c4b5fd;
1138    padding: 1px 5px;
1139    border-radius: 3px;
1140    font-size: 11px;
1141}
1142
1143.help-tip {
1144    margin-top: 16px;
1145    padding: 12px 14px;
1146    background: rgba(34, 197, 94, 0.1);
1147    border: 1px solid rgba(34, 197, 94, 0.3);
1148    border-radius: 8px;
1149}
1150
1151.help-tip-title {
1152    color: #22c55e;
1153    font-size: 12px;
1154    font-weight: 600;
1155    margin-bottom: 6px;
1156    display: flex;
1157    align-items: center;
1158    gap: 6px;
1159}
1160
1161.help-tip-text {
1162    color: #94a3b8;
1163    font-size: 12px;
1164    line-height: 1.5;
1165}
1166
1167.help-note {
1168    margin-top: 16px;
1169    padding: 12px 14px;
1170    background: rgba(139, 92, 246, 0.1);
1171    border-radius: 8px;
1172    color: #c4b5fd;
1173    font-size: 11px;
1174    line-height: 1.5;
1175}
1176
1177/* Download Buttons */
1178.download-section {
1179    margin-top: auto;
1180    padding-top: 8px;
1181}
1182
1183.download-btn {
1184    width: 100%;
1185    padding: 14px;
1186    background: linear-gradient(135deg, #8b5cf6, #6366f1);
1187    border: none;
1188    border-radius: 8px;
1189    color: white;
1190    font-size: 14px;
1191    font-weight: 600;
1192    cursor: pointer;
1193    transition: all 0.3s;
1194    display: flex;
1195    align-items: center;
1196    justify-content: center;
1197    gap: 8px;
1198}
1199
1200.download-btn:hover {
1201    transform: translateY(-2px);
1202    box-shadow: 0 5px 20px rgba(139, 92, 246, 0.4);
1203}
1204
1205.download-btn .btn-icon {
1206    font-size: 16px;
1207}
1208
1209/* Right Column - Preview */
1210.qr-preview-area {
1211    flex: 1;
1212    display: flex;
1213    flex-direction: column;
1214    align-items: center;
1215    justify-content: center;
1216    background: rgba(15, 23, 42, 0.5);
1217    border-radius: 12px;
1218    min-height: 320px;
1219    padding: 20px;
1220}
1221
1222.preview-label {
1223    color: #64748b;
1224    font-size: 10px;
1225    font-weight: 600;
1226    text-transform: uppercase;
1227    letter-spacing: 1px;
1228    margin-bottom: 16px;
1229}
1230
1231.preview-box {
1232    background: white;
1233    border-radius: 12px;
1234    padding: 0;
1235    overflow: hidden;
1236    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
1237}
1238
1239#qr-container {
1240    width: 240px;
1241    height: 240px;
1242    background: white;
1243    display: flex;
1244    align-items: center;
1245    justify-content: center;
1246}
1247
1248.preview-info {
1249    margin-top: 16px;
1250    color: #64748b;
1251    font-size: 11px;
1252    text-align: center;
1253}
1254
1255/* How-to Modal */
1256.howto-modal {
1257    display: none;
1258    position: fixed;
1259    top: 0;
1260    left: 0;
1261    width: 100%;
1262    height: 100%;
1263    background: rgba(0, 0, 0, 0.8);
1264    z-index: 2000;
1265    justify-content: center;
1266    align-items: center;
1267}
1268
1269.howto-content {
1270    background: #1e293b;
1271    border: 1px solid #8b5cf6;
1272    border-radius: 12px;
1273    padding: 25px;
1274    max-width: 420px;
1275    width: 90%;
1276    box-shadow: 0 0 30px rgba(139, 92, 246, 0.3);
1277}
1278
1279.howto-title {
1280    color: #8b5cf6;
1281    font-size: 18px;
1282    font-weight: 600;
1283    margin-bottom: 16px;
1284    text-align: center;
1285}
1286
1287.howto-step {
1288    display: flex;
1289    align-items: flex-start;
1290    gap: 10px;
1291    margin-bottom: 10px;
1292    color: #e2e8f0;
1293    font-size: 13px;
1294    line-height: 1.4;
1295}
1296
1297.howto-number {
1298    background: #8b5cf6;
1299    color: white;
1300    min-width: 55px;
1301    padding: 4px 8px;
1302    border-radius: 4px;
1303    display: flex;
1304    align-items: center;
1305    justify-content: center;
1306    font-size: 11px;
1307    font-weight: bold;
1308    flex-shrink: 0;
1309}
1310
1311.howto-intro {
1312    color: #94a3b8;
1313    font-size: 13px;
1314    margin-bottom: 16px;
1315    text-align: center;
1316}
1317
1318.howto-divider {
1319    height: 1px;
1320    background: #334155;
1321    margin: 16px 0;
1322}
1323
1324.howto-tip {
1325    margin-top: 12px;
1326    padding: 10px;
1327    background: rgba(139, 92, 246, 0.1);
1328    border-radius: 6px;
1329    color: #c4b5fd;
1330    font-size: 12px;
1331}
1332
1333.howto-btn {
1334    display: block;
1335    width: 100%;
1336    padding: 10px;
1337    margin-top: 16px;
1338    background: #8b5cf6;
1339    color: white;
1340    border: none;
1341    border-radius: 6px;
1342    font-size: 13px;
1343    font-weight: 500;
1344    cursor: pointer;
1345    transition: background 0.2s;
1346}
1347
1348.howto-btn:hover {
1349    background: #7c3aed;
1350}
1351
1352/* Download Modal */
1353.download-modal {
1354    display: none;
1355    position: fixed;
1356    top: 0;
1357    left: 0;
1358    width: 100%;
1359    height: 100%;
1360    background: rgba(0, 0, 0, 0.8);
1361    z-index: 2000;
1362    justify-content: center;
1363    align-items: center;
1364}
1365
1366.download-content {
1367    text-align: center;
1368}
1369
1370.qr-building-animation {
1371    display: grid;
1372    grid-template-columns: repeat(4, 1fr);
1373    gap: 4px;
1374    width: 60px;
1375    height: 60px;
1376    margin: 0 auto 15px;
1377}
1378
1379.qr-dot {
1380    background: #8b5cf6;
1381    border-radius: 2px;
1382    animation: pulse 0.6s ease-in-out infinite alternate;
1383}
1384
1385.qr-dot:nth-child(1) { animation-delay: 0s; }
1386.qr-dot:nth-child(2) { animation-delay: 0.1s; }
1387.qr-dot:nth-child(3) { animation-delay: 0.2s; }
1388.qr-dot:nth-child(4) { animation-delay: 0.3s; }
1389.qr-dot:nth-child(5) { animation-delay: 0.1s; }
1390.qr-dot:nth-child(6) { animation-delay: 0.2s; }
1391.qr-dot:nth-child(7) { animation-delay: 0.3s; }
1392.qr-dot:nth-child(8) { animation-delay: 0.4s; }
1393.qr-dot:nth-child(9) { animation-delay: 0.2s; }
1394.qr-dot:nth-child(10) { animation-delay: 0.3s; }
1395.qr-dot:nth-child(11) { animation-delay: 0.4s; }
1396.qr-dot:nth-child(12) { animation-delay: 0.5s; }
1397.qr-dot:nth-child(13) { animation-delay: 0.3s; }
1398.qr-dot:nth-child(14) { animation-delay: 0.4s; }
1399.qr-dot:nth-child(15) { animation-delay: 0.5s; }
1400.qr-dot:nth-child(16) { animation-delay: 0.6s; }
1401
1402@keyframes pulse {
1403    0% { opacity: 0.3; transform: scale(0.8); }
1404    100% { opacity: 1; transform: scale(1); }
1405}
1406
1407.download-text {
1408    color: #e2e8f0;
1409    font-size: 15px;
1410    font-weight: 500;
1411}
1412
1413.download-dots::after {
1414    content: '';
1415    animation: dots 1.5s steps(4, end) infinite;
1416}
1417
1418@keyframes dots {
1419    0%, 20% { content: ''; }
1420    40% { content: '.'; }
1421    60% { content: '..'; }
1422    80%, 100% { content: '...'; }
1423}
1424
1425/* Responsive */
1426@media (max-width: 1100px) {
1427    .qr-popup {
1428        width: 95vw;
1429    }
1430}
1431
1432@media (max-width: 820px) {
1433    /* Full-screen popup on mobile */
1434    .qr-backdrop {
1435        align-items: stretch;
1436        justify-content: stretch;
1437        padding: 0;
1438    }
1439    
1440    .qr-popup {
1441        width: 100vw;
1442        max-width: 100vw;
1443        height: 100vh;
1444        max-height: 100vh;
1445        border-radius: 0;
1446        border: none;
1447        box-shadow: none;
1448    }
1449    
1450    /* Compact header */
1451    .qr-header {
1452        padding: 12px 16px;
1453    }
1454    
1455    .qr-title {
1456        font-size: 16px;
1457    }
1458    
1459    /* Scrollable tab bar */
1460    .qr-tabs-wrapper {
1461        position: relative;
1462    }
1463    
1464    .tabs-scroll-hint {
1465        display: block;
1466        position: absolute;
1467        top: 4px;
1468        right: 16px;
1469        font-size: 11px;
1470        color: #ffffff;
1471        letter-spacing: 0.5px;
1472        font-weight: 500;
1473        z-index: 10;
1474    }
1475    
1476    .qr-tabs {
1477        padding: 12px 16px;
1478        padding-top: 22px; /* Make room for hint */
1479        gap: 6px;
1480        overflow-x: auto;
1481        justify-content: flex-start;
1482        -webkit-overflow-scrolling: touch;
1483        scrollbar-width: none; /* Firefox */
1484    }
1485    
1486    .qr-tabs::-webkit-scrollbar {
1487        display: none; /* Chrome/Safari */
1488    }
1489    
1490    .qr-tab {
1491        padding: 10px 16px;
1492        font-size: 11px;
1493        flex-shrink: 0;
1494        white-space: nowrap;
1495    }
1496    
1497    .tab-stacked {
1498        font-size: 10px;
1499    }
1500    
1501    /* Scrollable body */
1502    .qr-body {
1503        flex-direction: column;
1504        overflow-y: auto;
1505        padding: 16px;
1506        gap: 16px;
1507        flex: 1;
1508    }
1509    
1510    .qr-controls {
1511        width: 100%;
1512        overflow-y: visible;
1513        max-height: none;
1514    }
1515    
1516    /* Fix QR code clipping on mobile */
1517    .qr-preview-area {
1518        min-height: auto;
1519        order: -1; /* Move preview to top on mobile */
1520        flex-shrink: 0;
1521        padding: 20px;
1522    }
1523    
1524    .preview-box {
1525        overflow: visible;
1526        padding: 15px;
1527    }
1528    /* Smaller shape buttons */
1529    .shape-btns {
1530        gap: 6px;
1531    }
1532    
1533    .shape-btn {
1534        padding: 8px 12px;
1535        font-size: 11px;
1536    }
1537    
1538    /* Download button full width */
1539    .download-btn {
1540        width: 100%;
1541    }
1542}
1543</style>
1544
1545</head>
1546<body>
1547    
1548<!-- Faded Homepage Background -->
1549<div class="homepage-bg">
1550    <nav class="top-nav">
1551        <a href="/" class="logo">LaserLab</a>
1552        <div class="nav-links">
1553            <a href="/">Home</a>
1554            <div class="nav-dropdown">
1555                <button class="nav-dropdown-btn has-active">
1556                    Tools <span class="nav-dropdown-arrow">▼</span>
1557                </button>
1558                <div class="nav-dropdown-menu">
1559                    <a href="/imageprep">ImagePrep</a>
1560                    <a href="/slicer">Slicer</a>
1561                    <a href="/qrcode" class="active">QR Code Generator</a>
1562                    <a href="/ruler">Ruler Generator</a>
1563                </div>
1564            </div>
1565            <a href="https://www.facebook.com/tnelaserlab" target="_blank" rel="noopener" style="display: inline-flex; align-items: center; gap: 5px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg> Contact</a>
1566        </div>
1567    </nav>
1568    
1569    <header class="home-header">
1570        <img src="/static/images/laserlab-logo.png?v=1775399981" alt="LaserLab" class="home-logo">
1571        <p class="home-tagline">Professional image preparation tools for laser engraving</p>
1572    </header>
1573    
1574    <main class="home-features">
1575        <div class="feature-box">
1576            <img src="/static/images/feature-imageprep.jpg?v=1775399980" alt="ImagePrep" class="feature-img">
1577            <div class="feature-info">
1578                <h2 class="feature-name">ImagePrep</h2>
1579                <p class="feature-desc">Crop, Resize, Adjust, Dither images. Plus tools for removing spots, hair etc.</p>
1580            </div>
1581        </div>
1582        
1583        <div class="feature-box green">
1584            <img src="/static/images/feature-slicer.jpg?v=1775399980" alt="Slicer" class="feature-img">
1585            <div class="feature-info">
1586                <h2 class="feature-name">Slicer</h2>
1587                <p class="feature-desc">Slice images for multi-tile engravings with automatic gap compensation for perfect alignment.</p>
1588            </div>
1589        </div>
1590        
1591        <div class="mini-tools-column">
1592            <div class="feature-box cyan mini">
1593                <img src="/static/images/feature-ruler.jpg?v=1775399980" alt="Ruler Generator" class="feature-img">
1594                <div class="feature-info">
1595                    <h2 class="feature-name">Ruler Generator</h2>
1596                    <p class="feature-desc">Create custom rulers. Download laser-ready SVGs with cm/inch scales.</p>
1597                </div>
1598            </div>
1599            
1600            <div class="feature-box purple mini">
1601                <img src="/static/images/feature-qrcode.jpg?v=1775399980" alt="QR Code Generator" class="feature-img">
1602                <div class="feature-info">
1603                    <h2 class="feature-name">QR Code Generator</h2>
1604                    <p class="feature-desc">URL, WiFi, vCard & Google Review QR codes.</p>
1605                </div>
1606            </div>
1607        </div>
1608    </main>
1609</div>
1610
1611<!-- Popup overlay -->
1612<div class="qr-backdrop">
1613    <div class="qr-popup">
1614        <!-- Header -->
1615        <div class="qr-header">
1616            <div class="qr-title">
1617                <span>📱</span> QR Code Generator
1618            </div>
1619            <a href="/" class="qr-close">✕</a>
1620        </div>
1621        
1622        <!-- Tab Bar -->
1623        <div class="qr-tabs-wrapper">
1624            <span class="tabs-scroll-hint">more →</span>
1625            <div class="qr-tabs">
1626                <button class="qr-tab active" data-tab="url">URL</button>
1627                <button class="qr-tab" data-tab="wifi">WIFI</button>
1628                <button class="qr-tab" data-tab="vcard">VCARD</button>
1629                <button class="qr-tab" data-tab="review">
1630                    <span class="tab-stacked">GOOGLE<br>REVIEW</span>
1631                </button>
1632            </div>
1633        </div>
1634        
1635        <!-- Body -->
1636        <div class="qr-body">
1637            <!-- Left Column - Controls -->
1638            <div class="qr-controls">
1639                
1640                <!-- URL Tab Content -->
1641                <div class="tab-content active" id="tab-url">
1642                    <div class="control-group">
1643                        <div class="control-label">
1644                            <span class="label-icon">🔗</span> URL / Content
1645                        </div>
1646                        <textarea class="qr-input" id="qr-content" placeholder="Enter URL or text...">https://laserlab.app</textarea>
1647                    </div>
1648                </div>
1649                
1650                <!-- WiFi Tab Content -->
1651                <div class="tab-content" id="tab-wifi">
1652                    <div class="control-group">
1653                        <div class="control-label">
1654                            <span class="label-icon">📶</span> Network Name (SSID)
1655                        </div>
1656                        <input type="text" class="qr-input" id="wifi-ssid" placeholder="Enter network name">
1657                    </div>
1658                    
1659                    <div class="control-group">
1660                        <div class="control-label">
1661                            <span class="label-icon">🔑</span> Password
1662                        </div>
1663                        <div class="password-input-wrapper">
1664                            <input type="password" class="qr-input" id="wifi-p
1664assword" placeholder="Enter password (if required)">
1665                            <button type="button" class="password-toggle" id="password-toggle" title="Show/Hide password">
1666                                <span class="eye-icon">👁</span>
1667                            </button>
1668                        </div>
1669                    </div>
1670                    
1671                    <div class="control-group">
1672                        <div class="control-label">
1673                            <span class="label-icon">🔒</span> Security Type
1674                            <button type="button" class="help-icon" id="security-help-btn" title="How to find your security type">?</button>
1675                        </div>
1676                        <div class="security-btns">
1677                            <button class="security-btn active" data-security="WPA">WPA/WPA2/WPA3</button>
1678                            <button class="security-btn" data-security="WEP">WEP</button>
1679                            <button class="security-btn" data-security="nopass">None</button>
1680                        </div>
1681                    </div>
1682                    
1683                    <div class="control-group">
1684                        <div class="checkbox-row">
1685                            <span class="checkbox-label">Hidden Network</span>
1686                            <label class="toggle-switch">
1687                                <input type="checkbox" id="wifi-hidden">
1688                                <span class="toggle-slider"></span>
1689                            </label>
1690                        </div>
1691                        <div class="checkbox-hint">Enable if network doesn't broadcast its name</div>
1692                    </div>
1693                </div>
1694                
1695                <!-- vCard Tab Content -->
1696                <div class="tab-content" id="tab-vcard">
1697                    <div class="control-group">
1698                        <div class="control-label">
1699                            <span class="label-icon">👤</span> Display As
1700                        </div>
1701                        <div class="display-as-btns">
1702                            <button class="display-as-btn active" data-display="name">Name</button>
1703                            <button class="display-as-btn" data-display="company">Company Name</button>
1704                        </div>
1705                    </div>
1706                    
1707                    <div class="vcard-fields-scroll">
1708                        <div class="control-group vcard-name-fields">
1709                            <div class="control-label">
1710                                <span class="label-icon">✏️</span> Name
1711                            </div>
1712                            <div class="vcard-row">
1713                                <input type="text" class="qr-input" id="vcard-firstname" placeholder="First name">
1714                                <input type="text" class="qr-input" id="vcard-lastname" placeholder="Last name">
1715                            </div>
1716                        </div>
1717                        
1718                        <div class="control-group">
1719                            <div class="control-label">
1720                                <span class="label-icon">🏢</span> Company
1721                            </div>
1722                            <input type="text" class="qr-input" id="vcard-company" placeholder="Company name">
1723                        </div>
1724                        
1725                        <div class="control-group vcard-name-fields">
1726                            <div class="control-label">
1727                                <span class="label-icon">💼</span> Job Title
1728                            </div>
1729                            <input type="text" class="qr-input" id="vcard-title" placeholder="Job title">
1730                        </div>
1731                        
1732                        <div class="control-group">
1733                            <div class="control-label">
1734                                <span class="label-icon">📱</span> Phone
1735                            </div>
1736                            <div class="vcard-row">
1737                                <input type="tel" class="qr-input" id="vcard-mobile" placeholder="Mobile">
1738                                <input type="tel" class="qr-input" id="vcard-work" placeholder="Work">
1739                            </div>
1740                        </div>
1741                        
1742                        <div class="control-group">
1743                            <div class="control-label">
1744                                <span class="label-icon">✉️</span> Email
1745                            </div>
1746                            <input type="email" class="qr-input" id="vcard-email" placeholder="[email protected]">
1747                        </div>
1748                        
1749                        <div class="control-group">
1750                            <div class="control-label">
1751                                <span class="label-icon">🌐</span> Website
1752                            </div>
1753                            <input type="url" class="qr-input" id="vcard-website" placeholder="https://example.com">
1754                        </div>
1755                        
1756                        <div class="control-group">
1757                            <div class="control-label">
1758                                <span class="label-icon">📍</span> Address
1759                            </div>
1760                            <input type="text" class="qr-input" id="vcard-street" placeholder="Street address">
1761                            <div class="vcard-row" style="margin-top:8px;">
1762                                <input type="text" class="qr-input" id="vcard-city" placeholder="City">
1763                                <input type="text" class="qr-input" id="vcard-postcode" placeholder="Postcode">
1764                            </div>
1765                            <input type="text" class="qr-input" id="vcard-country" placeholder="Country" style="margin-top:8px;">
1766                        </div>
1767                    </div>
1768                </div>
1769                
1770                <!-- Review Tab Content -->
1771                <div class="tab-content" id="tab-review">
1772                    <div class="review-info-link" id="show-google-places-info">
1773                        <span>ℹ️</span> New to Google Reviews? <span class="info-link-action">Learn how to get started →</span>
1774                    </div>
1775                    
1776                    <div class="control-group">
1777                        <div class="control-label">
1778                            <span class="label-icon">🏪</span> Business Name
1779                        </div>
1780                        <input type="text" class="qr-input" id="review-business" placeholder="e.g. Nia's Diner">
1781                        <div class="input-hint">Optional - for your reference only</div>
1782                    </div>
1783                    
1784                    <div class="control-group">
1785                        <div class="control-label">
1786                            <span class="label-icon">📍</span> Google Place ID
1787                            <button type="button" class="help-icon" id="placeid-help-btn" title="How to find your Place ID">?</button>
1788                        </div>
1789                        <input type="text" class="qr-input" id="review-placeid" placeholder="ChIJ...">
1790                        <div class="input-hint">Required - starts with "ChIJ"</div>
1791                    </div>
1792                    
1793                    <div class="review-preview-url" id="review-url-preview">
1794                        Enter Place ID to generate review link
1795                    </div>
1796                </div>
1797                <div class="section-divider"></div>
1798                
1799                <!-- Size -->
1800                <div class="control-group">
1801                    <div class="control-label">
1802                        <span class="label-icon">📐</span> Size
1803                    </div>
1804                    <div class="input-row">
1805                        <input type="number" class="qr-input" id="qr-size" value="77" min="10" max="300">
1806                        <span class="input-suffix">mm</span>
1807                    </div>
1808                </div>
1809                
1810                <!-- Style Section -->
1811                <div class="control-group">
1812                    <div class="style-header">Style Options</div>
1813                    
1814                    <!-- Shape -->
1815                    <div class="control-label">
1816                        <span class="label-icon">🔷</span> Shape
1817                    </div>
1818                    <div class="shape-btns">
1819                        <!-- Blocky house for URL tab only -->
1820                        <button class="shape-btn shape-url-only" data-shape="house" title="House - blocky outline">
1821                            <span class="shape-icon">🏠</span>
1822                            House
1823                        </button>
1824                        <!-- WiFi houses for WiFi tab only -->
1825                        <button class="shape-btn shape-wifi-only" data-shape="houseA" title="House A - WiFi text at top" style="display:none;">
1826                            <span class="shape-icon">🏠</span>
1827                            House A
1828                        </button>
1829                        <button class="shape-btn shape-wifi-only" data-shape="houseB" title="House B - WiFi text at bottom" style="display:none;">
1830                            <span class="shape-icon">🏡</span>
1831                            House B
1832                        </button>
1833                        <button class="shape-btn active" data-shape="square">
1834                            <span class="shape-icon">◼</span>
1835                            Square
1836                        </button>
1837                        <button class="shape-btn" data-shape="circle">
1838                            <span class="shape-icon">●</span>
1839                            Circle
1840                        </button>
1841                    </div>
1842                </div>
1843                
1844                <!-- Format -->
1845                <div class="control-group">
1846                    <div class="control-label">
1847                        <span class="label-icon">💾</span> Format
1848                    </div>
1849                    <div class="format-toggle">
1850                        <button class="format-btn active" data-format="svg">SVG</button>
1851                        <button class="format-btn" data-format="png">PNG</button>
1852                    </div>
1853                </div>
1854                
1855                <!-- Download -->
1856                <div class="download-section">
1857                    <button class="download-btn" id="download-btn">
1858                        <span class="btn-icon">⬇</span> Download
1859                    </button>
1860                </div>
1861                
1862            </div>
1863            
1864            <!-- Right Column - Preview -->
1865            <div class="qr-preview-area">
1866                <div class="preview-label">Preview</div>
1867                <div class="preview-box">
1868                    <div id="qr-container"></div>
1869                </div>
1870                <div class="preview-info" id="preview-info">
1871                    77mm × 77mm • SVG
1872                </div>
1873            </div>
1874        </div>
1875    </div>
1876</div>
1877
1878<!-- How-to Modal -->
1879<div id="howto-modal" class="howto-modal">
1880    <div class="howto-content">
1881        <div class="howto-title">📱 QR Code Generator</div>
1882        
1883        <div class="howto-intro">Create laser-ready QR codes in multiple formats:</div>
1884        
1885        <div class="howto-step">
1886            <div class="howto-number">URL</div>
1887            <div>Website links or plain text</div>
1888        </div>
1889        
1890        <div class="howto-step">
1891            <div class="howto-number">WIFI</div>
1892            <div>Network credentials - scan to connect</div>
1893        </div>
1894        
1895        <div class="howto-step">
1896            <div class="howto-number">VCARD</div>
1897            <div>Contact cards - scan to save to phone</div>
1898        </div>
1899        
1900        <div class="howto-step">
1901            <div class="howto-number" style="font-size:9px;padding:6px 4px;">REVIEW</div>
1902            <div>Google Review links for your business</div>
1903        </div>
1904        
1905        <div class="howto-divider"></div>
1906        
1907        <div class="howto-tip">
1908            <strong>Shapes:</strong> Square, Circle, or House outlines<br>
1909            <strong>Formats:</strong> SVG or PNG with embedded DPI
1910        </div>
1911        
1912        <button class="howto-btn" id="howto-close-btn">Got it!</button>
1913    </div>
1914</div>
1915
1916<!-- Download Modal -->
1917<div id="download-modal" class="download-modal">
1918    <div class="download-content">
1919        <div class="qr-building-animation">
1920            <div class="qr-dot"></div>
1921            <div class="qr-dot"></div>
1922            <div class="qr-dot"></div>
1923            <div class="qr-dot"></div>
1924            <div class="qr-dot"></div>
1925            <div class="qr-dot"></div>
1926            <div class="qr-dot"></div>
1927            <div class="qr-dot"></div>
1928            <div class="qr-dot"></div>
1929            <div class="qr-dot"></div>
1930            <div class="qr-dot"></div>
1931            <div class="qr-dot"></div>
1932            <div class="qr-dot"></div>
1933            <div class="qr-dot"></div>
1934            <div class="qr-dot"></div>
1935            <div class="qr-dot"></div>
1936        </div>
1937        <div class="download-text">Building QR Code<span class="download-dots"></span></div>
1938    </div>
1939</div>
1940
1941<!-- Security Type Help Modal -->
1942<div id="security-help-modal" class="security-help-modal">
1943    <div class="security-help-content">
1944        <div class="security-help-header">
1945            <div class="security-help-title">
1946                <span>🔒</span> How to Find Your WiFi Security Type
1947            </div>
1948            <button class="security-help-close" id="security-help-close">✕</button>
1949        </div>
1950        <div class="security-help-body">
1951            
1952            <div class="help-section">
1953                <div class="help-section-title">
1954                    <span class="platform-icon">🍎</span> iPhone / iPad
1955                </div>
1956                <div class="help-steps">
1957                    <ol>
1958                        <li>Open <code>Settings</code></li>
1959                        <li>Tap <code>Wi-Fi</code></li>
1960                        <li>Tap the <code>ⓘ</code> icon next to your connected network</li>
1961                        <li>Look for "Security" — it will show WPA2, WPA3, WEP, or None</li>
1962                    </ol>
1963                </div>
1964            </div>
1965            
1966            <div class="help-section">
1967                <div class="help-section-title">
1968                    <span class="platform-icon">🤖</span> Android
1969                </div>
1970                <div class="help-steps">
1971                    <ol>
1972                        <li>Open <code>Settings</code></li>
1973                        <li>Tap <code>Network & Internet</code> → <code>Wi-Fi</code></li>
1974                        <li>Tap on your connected network name</li>
1975                        <li>Look for "Security" — shows WPA2, WPA3, WEP, or Open</li>
1976                    </ol>
1977                </div>
1978            </div>
1979            
1980            <div class="help-section">
1981                <div class="help-section-title">
1982                    <span class="platform-icon">🪟</span> Windows 10/11
1983                </div>
1984                <div class="help-steps">
1985                    <ol>
1986                        <li>Click the WiFi icon in taskbar</li>
1987                        <li>Click <code>Properties</code> under your network</li>
1988                        <li>Scroll down to find "Security type"</li>
1989                        <li>Shows WPA2-Personal, WPA3, WEP, or Open</li>
1990                    </ol>
1991                </div>
1992            </div>
1993            
1994            <div class="help-section">
1995                <div class="help-section-title">
1996                    <span class="platform-icon">🍏</span> Mac
1997                </div>
1998                <div class="help-steps">
1999                    <ol>
2000                        <li>Hold <code>Option</code> key and click WiFi icon in menu bar</li>
2001                        <li>Look for "Security" in the dropdown info</li>
2002                        <li>Shows WPA2 Personal, WPA3, WEP, or None</li>
2003                    </ol>
2004                </div>
2005            </div>
2006            
2007            <div class="help-tip">
2008                <div class="help-tip-title">
2009                    <span>📦</span> Check Your Router
2010                </div>
2011                <div class="help-tip-text">
2012                    Most routers have a label on the back or bottom showing the default WiFi name (SSID), password, and security type. Look for "Security: WPA2" or similar.
2013                </div>
2014            </div>
2015            
2016            <div class="help-note">
2017                <strong>💡 Quick Guide:</strong><br>
2018                • <strong>WPA/WPA2/WPA3</strong> — Most common, works with all modern routers<br>
2019                • <strong>WEP</strong> — Older security, rarely used today<br>
2020                • <strong>Open/None</strong> — No password required (public WiFi)
2021            </div>
2022            
2023        </div>
2024    </div>
2025</div>
2026
2027<!-- Place ID Help Modal -->
2028<div id="placeid-help-modal" class="placeid-help-modal">
2029    <div class="placeid-help-content">
2030        <div class="placeid-help-header">
2031            <div class="placeid-help-title">
2032                <span>📍</span> How to Find Your Google Place ID
2033            </div>
2034            <button class="placeid-help-close" id="placeid-help-close">✕</button>
2035        </div>
2036        <div class="placeid-help-body">
2037            
2038            <div class="placeid-step">
2039                <div class="placeid-step-num">1</div>
2040                <div class="placeid-step-content">
2041                    <h4>Open Google's Place ID Finder</h4>
2042                    <p>Click the link below to open Google's official tool:</p>
2043                    <a href="https://developers.google.com/maps/documentation/javascript/examples/places-placeid-finder" target="_blank" rel="noopener" class="placeid-url-link">
2044                        developers.google.com/maps/.../places-placeid-finder ↗
2045                    </a>
2046                </div>
2047            </div>
2048            
2049            <div class="placeid-step">
2050                <div class="placeid-step-num">2</div>
2051                <div class="placeid-step-content">
2052                    <h4>Search for Your Business</h4>
2053                    <p>Type your business name in the search box on the map. Include your city/location for accurate results.</p>
2054                </div>
2055            </div>
2056            
2057            <div class="placeid-step">
2058                <div class="placeid-step-num">3</div>
2059                <div class="placeid-step-content">
2060                    <h4>Click on Your Business</h4>
2061                    <p>Click on your business in the search results or on the map marker. A popup will appear with details.</p>
2062                </div>
2063            </div>
2064            
2065            <div class="placeid-step">
2066                <div class="placeid-step-num">4</div>
2067                <div class="placeid-step-content">
2068                    <h4>Copy the Place ID</h4>
2069                    <p>Find the <code>Place ID</code> field - it starts with <code>ChIJ</code>
2069. Copy this entire string.</p>
2070                </div>
2071            </div>
2072            
2073            <div class="placeid-example">
2074                <div class="placeid-example-title">
2075                    <span>✅</span> Example Place ID
2076                </div>
2077                <div class="placeid-example-text">ChIJIY5V-9cXbkgRJUTnmlBP0wM</div>
2078            </div>
2079            
2080            <div class="placeid-btn-row">
2081                <button class="placeid-close-btn" id="placeid-help-close-btn" style="flex:1;">Got it!</button>
2082            </div>
2083            
2084        </div>
2085    </div>
2086</div>
2087
2088<!-- Google Places Info Modal (shows on first click of Review tab) -->
2089<div id="google-places-modal" class="placeid-help-modal">
2090    <div class="placeid-help-content">
2091        <div class="placeid-help-header">
2092            <div class="placeid-help-title">
2093                <span>⭐</span> Google Review QR Codes
2094            </div>
2095            <button class="placeid-help-close" id="google-places-close">✕</button>
2096        </div>
2097        <div class="placeid-help-body">
2098            
2099            <div class="google-places-intro">
2100                To create a Google Review QR code, your business must be registered on <strong>Google Business Profile</strong> (formerly Google My Business).
2101            </div>
2102            
2103            <div class="placeid-step">
2104                <div class="placeid-step-num">?</div>
2105                <div class="placeid-step-content">
2106                    <h4>Not Sure If You're Registered?</h4>
2107                    <p>Search for your business on Google Maps. If it appears with your details, you're already listed!</p>
2108                </div>
2109            </div>
2110            
2111            <div class="placeid-step">
2112                <div class="placeid-step-num">+</div>
2113                <div class="placeid-step-content">
2114                    <h4>Register Your Business</h4>
2115                    <p>Create or claim your Google Business Profile:</p>
2116                    <a href="https://www.google.com/business/" target="_blank" rel="noopener" class="placeid-url-link">
2117                        google.com/business ↗
2118                    </a>
2119                </div>
2120            </div>
2121            
2122            <div class="placeid-step">
2123                <div class="placeid-step-num">📖</div>
2124                <div class="placeid-step-content">
2125                    <h4>Need Help?</h4>
2126                    <p>Google's official guide to getting started:</p>
2127                    <a href="https://support.google.com/business/answer/10514137" target="_blank" rel="noopener" class="placeid-url-link">
2128                        Google Business Profile Help ↗
2129                    </a>
2130                </div>
2131            </div>
2132            
2133            <div class="google-places-note">
2134                <strong>💡 Once registered:</strong> Use the <span class="help-icon-inline">?</span> button next to "Google Place ID" to find your unique Place ID.
2135            </div>
2136            
2137            <div class="placeid-btn-row">
2138                <button class="placeid-close-btn" id="google-places-close-btn" style="flex:1;">Got it!</button>
2139            </div>
2140            
2141        </div>
2142    </div>
2143</div>
2144
2145<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
vendor: 1 bytes, line 2145
2145
2146<script src="/static/js/qrcode-gen.js?v=1780941765"></script>
2146
2147
2148<!-- Tab switching and WiFi logic -->
2149<script>
2150document.addEventListener('DOMContentLoaded', function() {
2151    // Function to show/hide house shapes based on tab
2152    function updateShapeButtonsForTab(tabId) {
2153        const houseBtn = document.querySelector('.shape-btn[data-shape="house"]');      // Blocky house (URL)
2154        const houseABtn = document.querySelector('.shape-btn[data-shape="houseA"]');    // WiFi house A
2155        const houseBBtn = document.querySelector('.shape-btn[data-shape="houseB"]');    // WiFi house B
2156        const squareBtn = document.querySelector('.shape-btn[data-shape="square"]');
2157        
2158        if (tabId === 'url') {
2159            // URL tab: show blocky house, hide WiFi houses
2160            if (houseBtn) houseBtn.style.display = '';
2161            if (houseABtn) houseABtn.style.display = 'none';
2162            if (houseBBtn) houseBBtn.style.display = 'none';
2163            
2164            // If a WiFi house shape is active, switch to square
2165            const activeShape = document.querySelector('.shape-btn.active');
2166            if (activeShape && (activeShape.dataset.shape === 'houseA' || activeShape.dataset.shape === 'houseB')) {
2167                activeShape.classList.remove('active');
2168                if (squareBtn) {
2169                    squareBtn.classList.add('active');
2170                    if (typeof window.setShape === 'function') {
2171                        window.setShape('square');
2172                    }
2173                }
2174            }
2175        } else if (tabId === 'wifi') {
2176            // WiFi tab: show WiFi houses, hide blocky house
2177            if (houseBtn) houseBtn.style.display = 'none';
2178            if (houseABtn) houseABtn.style.display = '';
2179            if (houseBBtn) houseBBtn.style.display = '';
2180            
2181            // If blocky house is active, switch to square
2182            const activeShape = document.querySelector('.shape-btn.active');
2183            if (activeShape && activeShape.dataset.shape === 'house') {
2184                activeShape.classList.remove('active');
2185                if (squareBtn) {
2186                    squareBtn.classList.add('active');
2187                    if (typeof window.setShape === 'function') {
2188                        window.setShape('square');
2189                    }
2190                }
2191            }
2192        } else {
2193            // Review and vCard tabs: hide all house buttons
2194            if (houseBtn) houseBtn.style.display = 'none';
2195            if (houseABtn) houseABtn.style.display = 'none';
2196            if (houseBBtn) houseBBtn.style.display = 'none';
2197            
2198            // If any house shape is currently active, switch to square
2199            const activeShape = document.querySelector('.shape-btn.active');
2200            if (activeShape && (activeShape.dataset.shape === 'house' || activeShape.dataset.shape === 'houseA' || activeShape.dataset.shape === 'houseB')) {
2201                activeShape.classList.remove('active');
2202                if (squareBtn) {
2203                    squareBtn.classList.add('active');
2204                    if (typeof window.setShape === 'function') {
2205                        window.setShape('square');
2206                    }
2207                }
2208            }
2209        }
2210    }
2211    
2212    // Tab switching
2213    document.querySelectorAll('.qr-tab').forEach(tab => {
2214        tab.addEventListener('click', function() {
2215            const tabId = this.dataset.tab;
2216            
2217            // Update tab buttons
2218            document.querySelectorAll('.qr-tab').forEach(t => t.classList.remove('active'));
2219            this.classList.add('active');
2220            
2221            // Update tab content
2222            document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
2223            document.getElementById('tab-' + tabId).classList.add('active');
2224            
2225            // Update shape buttons visibility for this tab
2226            updateShapeButtonsForTab(tabId);
2227            
2228            // Show Google Places info modal on first click of Review tab
2229            if (tabId === 'review' && !localStorage.getItem('reviewTabInfoShown')) {
2230                const googlePlacesModal = document.getElementById('google-places-modal');
2231                if (googlePlacesModal) {
2232                    googlePlacesModal.style.display = 'flex';
2233                    localStorage.setItem('reviewTabInfoShown', 'true');
2234                }
2235            }
2236            
2237            // Update QR state and regenerate
2238            if (typeof updateQRFromActiveTab === 'function') {
2239                updateQRFromActiveTab();
2240            }
2241        });
2242    });
2243    
2244    // Password show/hide toggle
2245    const passwordToggle = document.getElementById('password-toggle');
2246    const passwordInput = document.getElementById('wifi-password');
2247    if (passwordToggle && passwordInput) {
2248        passwordToggle.addEventListener('click', function() {
2249            const isPassword = passwordInput.type === 'password';
2250            passwordInput.type = isPassword ? 'text' : 'password';
2251            this.querySelector('.eye-icon').textContent = isPassword ? '🙈' : '👁';
2252        });
2253    }
2254    
2255    // Security type buttons
2256    document.querySelectorAll('.security-btn').forEach(btn => {
2257        btn.addEventListener('click', function() {
2258            document.querySelectorAll('.security-btn').forEach(b => b.classList.remove('active'));
2259            this.classList.add('active');
2260            
2261            // Show/hide password field based on security type
2262            const securityType = this.dataset.security;
2263            const passwordGroup = document.getElementById('wifi-password').closest('.control-group');
2264            if (securityType === 'nopass') {
2265                passwordGroup.style.display = 'none';
2266            } else {
2267                passwordGroup.style.display = 'block';
2268            }
2269            
2270            // Regenerate QR
2271            if (typeof updateQRFromActiveTab === 'function') {
2272                updateQRFromActiveTab();
2273            }
2274        });
2275    });
2276    
2277    // WiFi inputs change handlers
2278    ['wifi-ssid', 'wifi-password'].forEach(id => {
2279        const el = document.getElementById(id);
2280        if (el) {
2281            el.addEventListener('input', function() {
2282                if (typeof updateQRFromActiveTab === 'function') {
2283                    // Debounce
2284                    clearTimeout(window.wifiDebounce);
2285                    window.wifiDebounce = setTimeout(updateQRFromActiveTab, 300);
2286                }
2287            });
2288        }
2289    });
2290    
2291    // Hidden network toggle
2292    const hiddenToggle = document.getElementById('wifi-hidden');
2293    if (hiddenToggle) {
2294        hiddenToggle.addEventListener('change', function() {
2295            if (typeof updateQRFromActiveTab === 'function') {
2296                updateQRFromActiveTab();
2297            }
2298        });
2299    }
2300    
2301    // vCard Display As buttons
2302    document.querySelectorAll('.display-as-btn').forEach(btn => {
2303        btn.addEventListener('click', function() {
2304            document.querySelectorAll('.display-as-btn').forEach(b => b.classList.remove('active'));
2305            this.classList.add('active');
2306            if (typeof updateQRFromActiveTab === 'function') {
2307                updateQRFromActiveTab();
2308            }
2309        });
2310    });
2311    
2312    // vCard input change handlers
2313    const vcardInputIds = [
2314        'vcard-firstname', 'vcard-lastname', 'vcard-company', 'vcard-title',
2315        'vcard-mobile', 'vcard-work', 'vcard-email', 'vcard-website',
2316        'vcard-street', 'vcard-city', 'vcard-postcode', 'vcard-country'
2317    ];
2318    vcardInputIds.forEach(id => {
2319        const el = document.getElementById(id);
2320        if (el) {
2321            el.addEventListener('input', function() {
2322                if (typeof updateQRFromActiveTab === 'function') {
2323                    clearTimeout(window.vcardDebounce);
2324                    window.vcardDebounce = setTimeout(updateQRFromActiveTab, 300);
2325                }
2326            });
2327        }
2328    });
2329    
2330    // Security Help Modal
2331    const securityHelpBtn = document.getElementById('security-help-btn');
2332    const securityHelpModal = document.getElementById('security-help-modal');
2333    const securityHelpClose = document.getElementById('security-help-close');
2334    
2335    if (securityHelpBtn && securityHelpModal) {
2336        securityHelpBtn.addEventListener('click', function(e) {
2337            e.preventDefault();
2338            e.stopPropagation();
2339            securityHelpModal.style.display = 'flex';
2340        });
2341        
2342        securityHelpClose.addEventListener('click', function() {
2343            securityHelpModal.style.display = 'none';
2344        });
2345        
2346        // Close on backdrop click
2347        securityHelpModal.addEventListener('click', function(e) {
2348            if (e.target === securityHelpModal) {
2349                securityHelpModal.style.display = 'none';
2350            }
2351        });
2352        
2353        // Close on Escape key
2354        document.addEventListener('keydown', function(e) {
2355            if (e.key === 'Escape' && securityHelpModal.style.display === 'flex') {
2356                securityHelpModal.style.display = 'none';
2357            }
2358        });
2359    }
2360    
2361    // Place ID Help Modal
2362    const placeidHelpBtn = document.getElementById('placeid-help-btn');
2363    const placeidHelpModal = document.getElementById('placeid-help-modal');
2364    const placeidHelpClose = document.getElementById('placeid-help-close');
2365    const placeidHelpCloseBtn = document.getElementById('placeid-help-close-btn');
2366    
2367    if (placeidHelpBtn && placeidHelpModal) {
2368        placeidHelpBtn.addEventListener('click', function(e) {
2369            e.preventDefault();
2370            e.stopPropagation();
2371            placeidHelpModal.style.display = 'flex';
2372        });
2373        
2374        placeidHelpClose.addEventListener('click', function() {
2375            placeidHelpModal.style.display = 'none';
2376        });
2377        
2378        placeidHelpCloseBtn.addEventListener('click', function() {
2379            placeidHelpModal.style.display = 'none';
2380        });
2381        
2382        // Close on backdrop click
2383        placeidHelpModal.addEventListener('click', function(e) {
2384            if (e.target === placeidHelpModal) {
2385                placeidHelpModal.style.display = 'none';
2386            }
2387        });
2388        
2389        // Close on Escape key
2390        document.addEventListener('keydown', function(e) {
2391            if (e.key === 'Escape' && placeidHelpModal.style.display === 'flex') {
2392                placeidHelpModal.style.display = 'none';
2393            }
2394        });
2395    }
2396    
2397    // Google Places Info Modal
2398    const googlePlacesModal = document.getElementById('google-places-modal');
2399    const googlePlacesClose = document.getElementById('google-places-close');
2400    const googlePlacesCloseBtn = document.getElementById('google-places-close-btn');
2401    
2402    if (googlePlacesModal) {
2403        if (googlePlacesClose) {
2404            googlePlacesClose.addEventListener('click', function() {
2405                googlePlacesModal.style.display = 'none';
2406            });
2407        }
2408        
2409        if (googlePlacesCloseBtn) {
2410            googlePlacesCloseBtn.addEventListener('click', function() {
2411                googlePlacesModal.style.display = 'none';
2412            });
2413        }
2414        
2415        // Close on backdrop click
2416        googlePlacesModal.addEventListener('click', function(e) {
2417            if (e.target === googlePlacesModal) {
2418                googlePlacesModal.style.display = 'none';
2419            }
2420        });
2421        
2422        // Close on Escape key
2423        document.addEventListener('keydown', function(e) {
2424            if (e.key === 'Escape' && googlePlacesModal.style.display === 'flex') {
2425                googlePlacesModal.style.display = 'none';
2426            }
2427        });
2428        
2429        // Info link to show modal on demand
2430        const showGooglePlacesInfo = document.getElementById('show-google-places-info');
2431        if (showGooglePlacesInfo) {
2432            showGooglePlacesInfo.addEventListener('click', function() {
2433                googlePlacesModal.style.display = 'flex';
2434            });
2435        }
2436    }
2437    
2438    // Review tab input handlers
2439    const reviewPlaceidInput = document.getElementById('review-placeid');
2440    const reviewBusinessInput = document.getElementById('review-business');
2441    const reviewUrlPreview = document.getElementById('review-url-preview');
2442    
2443    function updateReviewUrlPreview() {
2444        const placeid = reviewPlaceidInput ? reviewPlaceidInput.value.trim() : '';
2445        
2446        if (placeid && placeid.startsWith('ChIJ')) {
2447            const reviewUrl = 'https://search.google.com/local/writereview?placeid=' + placeid;
2448            reviewUrlPreview.textContent = reviewUrl;
2449            reviewUrlPreview.classList.add('valid');
2450        } else if (placeid) {
2451            reviewUrlPreview.textContent = 'Place ID should start with "ChIJ"';
2452            reviewUrlPreview.classList.remove('valid');
2453        } else {
2454            reviewUrlPreview.textContent = 'Enter Place ID to generate review link';
2455            reviewUrlPreview.classList.remove('valid');
2456        }
2457    }
2458    
2459    if (reviewPlaceidInput) {
2460        reviewPlaceidInput.addEventListener('input', function() {
2461            updateReviewUrlPreview();
2462            if (typeof updateQRFromActiveTab === 'function') {
2463                clearTimeout(window.reviewDebounce);
2464                window.reviewDebounce = setTimeout(updateQRFromActiveTab, 300);
2465            }
2466        });
2467    }
2468    
2469    if (reviewBusinessInput) {
2470        reviewBusinessInput.addEventListener('input', function() {
2471            // Business name is optional, just for reference
2472            // Could be used later for display purposes
2473        });
2474    }
2475    
2476    // Update preview info when size or format changes
2477    function updatePreviewInfo() {
2478        const size = document.getElementById('qr-size').value || 77;
2479        const format = document.querySelector('.format-btn.active').dataset.format.toUpperCase();
2480        const shape = document.querySelector('.shape-btn.active').dataset.shape;
2481        
2482        let dims = size + 'mm × ' + size + 'mm';
2483        if (shape === 'house') {
2484            // Blocky house has aspect ratio based on 109.91mm x 105.43mm viewBox
2485            const height = Math.round(size * (105.43 / 109.91));
2486            dims = size + 'mm × ' + height + 'mm';
2487        } else if (shape === 'houseA' || shape === 'houseB') {
2488            // House shapes have aspect ratio based on 114.7mm x 125.4mm viewBox
2489            const height = Math.round(size * (125.4 / 114.7));
2490            dims = size + 'mm × ' + height + 'mm';
2491        }
2492        
2493        document.getElementById('preview-info').textContent = dims + ' • ' + format;
2494    }
2495    
2496    document.getElementById('qr-size').addEventListener('input', updatePreviewInfo);
2497    document.querySelectorAll('.format-btn').forEach(btn => {
2498        btn.addEventListener('click', updatePreviewInfo);
2499    });
2500    document.querySelectorAll('.shape-btn').forEach(btn => {
2501        btn.addEventListener('click', function() {
2502            setTimeout(updatePreviewInfo, 50);
2503        });
2504    });
2505    
2506});
2507</script>
2507
2508
2509    
2510    <!-- Scripts at end of body for faster page render -->
2511    <!-- Fabric.js - loaded async from LaserLab CDN -->
2512    
2512<script>
2513        // Load Fabric.js asynchronously
2514        (function() {
2515            var script = document.createElement('script');
2516            script.src = 'https://assets.laserlab.app/fabric.min.js';
2517            script.async = true;
2518            script.crossOrigin = 'anonymous';
2519            script.onload = function() {
2520                console.log('✅ Fabric.js loaded');
2521                // Dispatch event so imageprep.js knows Fabric is ready
2522                window.dispatchEvent(new Event('fabricReady'));
2523            };
2524            script.onerror = function() {
2525                console.error('❌ Fabric.js failed from R2, trying cdnjs fallback');
2526                var fallback = document.createElement('script');
2527                fallback.src = 'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js';
2528                fallback.async = true;
2529                fallback.onload = function() {
2530                    console.log('✅ Fabric.js loaded (cdnjs fallback)');
2531                    window.dispatchEvent(new Event('fabricReady'));
2532                };
2533                document.head.appendChild(fallback);
2534            };
2535            document.head.appendChild(script);
2536        })();
2537    </script>
2537
2538    
2539    <!-- noUiSlider - small, load normally -->
2540    
2540<script src="https://assets.laserlab.app/nouislider.min.js"></script>
2540
2541    
2542    <!-- OpenCV.js - Preloaded in background after page loads -->
2543    
2543<script>
2544        window.cvReady = false;
2545        let cvReady = false;
2546        let cvLoading = false;
2547        
2548        function loadOpenCV(callback) {
2549            // Already loaded
2550            if (cvReady) {
2551                if (callback) callback();
2552                return;
2553            }
2554            
2555            // Already loading - queue callback
2556            if (cvLoading) {
2557                const checkReady = setInterval(() => {
2558                    if (cvReady) {
2559                        clearInterval(checkReady);
2560                        if (callback) callback();
2561                    }
2562                }, 100);
2563                return;
2564            }
2565            
2566            // Start loading
2567            cvLoading = true;
2568            console.log('⏳ Loading OpenCV.js (full build with inpaint)...');
2569            
2570            const script = document.createElement('script');
2571            // Primary: LaserLab Cloudflare R2 bucket (fast, reliable, brotli compressed)
2572            script.src = 'https://assets.laserlab.app/opencv.js';
2573            script.async = true;
2574            script.crossOrigin = 'anonymous';
2575            script.onload = function() {
2576                console.log('OpenCV script loaded, waiting for runtime...');
2577                if (typeof cv !== 'undefined') {
2578                    if (typeof cv.onRuntimeInitialized === 'function' || cv.onRuntimeInitialized === undefined) {
2579                        cv['onRuntimeInitialized'] = () => {
2580                            cvReady = true;
2581                            window.cvReady = true;
2582                            cvLoading = false;
2583                            console.log('✅ OpenCV.js READY - cv.inpaint available:', typeof cv.inpaint === 'function');
2584                            if (callback) callback();
2585                        };
2586                    } else {
2587                        // Already initialized
2588                        cvReady = true;
2589                        window.cvReady = true;
2590                        cvLoading = false;
2591                        console.log('✅ OpenCV.js READY (immediate) - cv.inpaint available:', typeof cv.inpaint === 'function');
2592                        if (callback) callback();
2593                    }
2594                }
2595            };
2596            script.onerror = function() {
2597                cvLoading = false;
2598                console.error('❌ Failed to load OpenCV.js from assets.laserlab.app');
2599                // Fallback 1: docs.opencv.org
2600                console.log('Trying docs.opencv.org fallback...');
2601                const fallback1 = document.createElement('script');
2602                fallback1.src = 'https://docs.opencv.org/4.x/opencv.js';
2603                fallback1.async = true;
2604                fallback1.onload = function() {
2605                    if (typeof cv !== 'undefined') {
2606                        cv['onRuntimeInitialized'] = () => {
2607                            cvReady = true;
2608                            window.cvReady = true;
2609                            cvLoading = false;
2610                            console.log('✅ OpenCV.js READY (opencv.org fallback) - cv.inpaint available:', typeof cv.inpaint === 'function');
2611                            if (callback) callback();
2612                        };
2613                    }
2614                };
2615                fallback1.onerror = function() {
2616                    // Fallback 2: jsdelivr (may not have inpaint)
2617                    console.log('Trying jsdelivr fallback (may not have inpaint)...');
2618                    const fallback2 = document.createElement('script');
2619                    fallback2.src = 'https://cdn.jsdelivr.net/npm/[email protected]/opencv.min.js';
2620                    fallback2.async = true;
2621                    fallback2.onload = function() {
2622                        if (typeof cv !== 'undefined') {
2623                            cv['onRuntimeInitialized'] = () => {
2624                                cvReady = true;
2625                                window.cvReady = true;
2626                                console.log('✅ OpenCV.js READY (jsdelivr fallback) - cv.inpaint available:', typeof cv.inpaint === 'function');
2627                                if (callback) callback();
2628                            };
2629                        }
2630                    };
2631                    document.body.appendChild(fallback2);
2632                };
2633                document.body.appendChild(fallback1);
2634            };
2635            document.body.appendChild(script);
2636        }
2637        
2638        // Preload OpenCV in background - ONLY on ImagePrep page
2639        // Slicer doesn't need OpenCV and loading it slows down the page
2640        window.addEventListener('load', function() {
2641            const isImagePrep = window.location.pathname.includes('imageprep');
2642            
2643            if (isImagePrep) {
2644                // Load immediately for imageprep (needed for spot removal, auto-enhance)
2645                setTimeout(function() {
2646                    if (!cvReady && !cvLoading) {
2647                        console.log('🔄 Preloading OpenCV.js for ImagePrep...');
2648                        loadOpenCV();
2649                    }
2650                }, 100);
2651            }
2652            // Don't preload on other pages - they don't need it
2653        });
2654    </script>
2654
2655    
2656    
2657    
2658    <!-- Service Worker Registration (caches big libraries for faster repeat visits) -->
2659    
2659<script>
2660        if ('serviceWorker' in navigator) {
2661            window.addEventListener('load', function() {
2662                navigator.serviceWorker.register('/sw.js')
2663                    .then(function(registration) {
2664                        console.log('✅ Service Worker registered');
2665                    })
2666                    .catch(function(error) {
2667                        console.log('Service Worker registration failed:', error);
2668                    });
2669            });
2670        }
2671    </script>
vendor: 1 bytes, line 2671
2671
2672<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"5428fc1abd69496794171f0c809c326a","r":1,"spa":2}' crossorigin="anonymous"></script>
2672
2673</body>
2674</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.