-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSection 01 Checklist.html
More file actions
847 lines (761 loc) · 49.6 KB
/
Copy pathSection 01 Checklist.html
File metadata and controls
847 lines (761 loc) · 49.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Section 01 — Cover Drawing & Signature Drawing Checklist</title>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<style>
*, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }
:root {
--navy:#0f2744; --navy-light:#1a3a5c; --navy-hover:#1f4570;
--green:#2d9e6b; --green-light:#e8f7f1;
--bg:#f2efe9; --card:#fff; --text:#1c1c1c; --muted:#888;
--border:#e0dbd2; --danger:#b83232;
--modal:#fff; --surf:var(--card); --surf2:var(--bg); --surf3:#e8e4dc;
--bd:var(--border); --bd-i:#d0cbc2;
--txt:var(--text); --txt-b:#0f2744; --txt-m:#555; --txt-d:var(--muted); --txt-dd:#bbb;
--inp:#f9f7f4;
--ghost-bg:#f5f3ef; --ghost-c:var(--muted); --ghost-bd:var(--border);
--child-bg:#fdfcfb; --tree-line:#d4cec6;
}
body { font-family:'IBM Plex Sans',sans-serif; background:var(--bg); color:var(--text); min-height:100vh; padding:0 0 80px; }
/* ── HEADER ── */
header { background:var(--navy); color:#fff; padding:24px 40px 20px; position:sticky; top:0; z-index:100; box-shadow:0 2px 12px rgba(0,0,0,.25); display:flex; justify-content:space-between; align-items:center; gap:20px; }
.eyebrow { font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:2.5px; text-transform:uppercase; color:rgba(255,255,255,.5); margin-bottom:6px; }
.title-row { display:flex; align-items:baseline; flex-wrap:nowrap; gap:0; }
#projectTitle {
font-family:'IBM Plex Sans',sans-serif; font-size:19px; font-weight:600; letter-spacing:-.3px;
color:#fff; outline:none; border-bottom:1px dashed rgba(255,255,255,.2);
display:inline-block; white-space:nowrap; min-width:2px; max-width:320px;
cursor:text; transition:border-color .15s; vertical-align:baseline; line-height:1.3;
}
#projectTitle:focus { border-bottom-color:rgba(255,255,255,.6); }
#projectTitle:empty::before { content:attr(data-placeholder); color:rgba(255,255,255,.28); font-style:italic; pointer-events:none; }
.title-sep { font-size:19px; font-weight:400; color:rgba(255,255,255,.35); margin:0 7px; flex-shrink:0; line-height:1.3; }
.title-sep.hidden { display:none; }
.title-static { font-size:19px; font-weight:600; letter-spacing:-.3px; white-space:nowrap; flex-shrink:0; line-height:1.3; }
.export-timestamp { display:none; font-family:'IBM Plex Mono',monospace; font-size:10px; letter-spacing:1.5px; color:rgba(255,255,255,.45); margin-top:5px; }
.progress-block { text-align:right; flex-shrink:0; }
.progress-count { font-family:'IBM Plex Mono',monospace; font-size:26px; font-weight:600; color:#fff; line-height:1; }
.progress-label { font-size:10px; text-transform:uppercase; letter-spacing:2px; color:rgba(255,255,255,.45); margin-top:3px; margin-bottom:8px; }
.progress-bar-outer { background:rgba(255,255,255,.15); border-radius:99px; height:5px; width:140px; overflow:hidden; margin-left:auto; }
.progress-bar-inner { background:var(--green); height:5px; border-radius:99px; width:0%; transition:width .4s ease; }
/* ── TOOLBAR ── */
.toolbar { max-width:860px; margin:22px auto 0; padding:0 24px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; }
.filter-group { display:flex; border:1px solid var(--border); border-radius:6px; overflow:hidden; background:var(--card); }
.filter-btn { font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:1px; text-transform:uppercase; background:none; border:none; border-right:1px solid var(--border); color:var(--muted); padding:7px 15px; cursor:pointer; transition:all .15s; }
.filter-btn:last-child { border-right:none; }
.filter-btn:hover { color:var(--navy); background:#faf8f5; }
.filter-btn.active { background:var(--navy); color:#fff; }
.action-group { display:flex; gap:8px; }
.action-btn { font-family:'IBM Plex Mono',monospace; font-size:11px; letter-spacing:1px; text-transform:uppercase; background:var(--card); border:1px solid var(--border); color:var(--muted); padding:7px 14px; border-radius:6px; cursor:pointer; transition:all .15s; }
.action-btn:hover { border-color:var(--navy); color:var(--navy); }
.action-btn.danger:hover { border-color:var(--danger); color:var(--danger); }
.action-btn.highlight { background:rgba(45,158,107,.1); color:var(--green); border-color:rgba(45,158,107,.3); }
.action-btn.highlight:hover { background:rgba(45,158,107,.18); border-color:var(--green); }
/* ── SAVED ROW ── */
.saved-row { max-width:860px; margin:7px auto 0; padding:0 24px; display:flex; justify-content:flex-end; }
.saved-indicator { font-family:'IBM Plex Mono',monospace; font-size:10px; color:var(--muted); letter-spacing:.5px; min-height:14px; }
.saved-indicator.flash { color:var(--green); }
/* ── MAIN ── */
main { max-width:860px; margin:14px auto 0; padding:0 24px; }
.complete-banner { display:none; background:var(--green); color:#fff; text-align:center; padding:15px; border-radius:8px; margin-bottom:18px; font-weight:600; font-size:14px; }
.complete-banner.visible { display:block; }
/* ── SECTIONS ── */
.section { background:var(--card); border-radius:8px; margin-bottom:18px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,.07),0 2px 8px rgba(0,0,0,.04); }
.section-header { background:var(--navy-light); color:#fff; padding:13px 20px; display:flex; justify-content:space-between; align-items:center; cursor:pointer; user-select:none; transition:background .15s; }
.section-header:hover { background:var(--navy-hover); }
.section-header-left { display:flex; align-items:center; gap:10px; }
.collapse-icon { font-size:9px; opacity:.55; transition:transform .2s ease; flex-shrink:0; }
.section.collapsed .collapse-icon { transform:rotate(-90deg); }
.section-title { font-size:13px; font-weight:600; }
.section-badge { font-family:'IBM Plex Mono',monospace; font-size:11px; background:rgba(255,255,255,.18); padding:2px 10px; border-radius:99px; flex-shrink:0; transition:background .25s; }
.section-badge.complete { background:var(--green); }
.section-items { max-height:99999px; overflow:hidden; transition:max-height .3s ease; }
.section.collapsed .section-items { max-height:0; }
/* ── CHECKBOX — three states: unchecked / indeterminate / checked ── */
.checkbox {
flex-shrink:0; margin-top:2px; width:17px; height:17px;
border:2px solid #c5bfb5; border-radius:3px; background:#fff;
display:flex; align-items:center; justify-content:center;
transition:all .15s; pointer-events:none;
}
.checkbox .check { display:none; }
.checkbox .dash { display:none; }
.item.checked .checkbox { background:var(--green); border-color:var(--green); }
.item.checked .checkbox .check { display:block; }
.item.indeterminate .checkbox { background:var(--navy-light); border-color:var(--navy-light); }
.item.indeterminate .checkbox .dash { display:block; }
/* ── STANDALONE ITEMS ── */
.item { display:flex; align-items:flex-start; gap:14px; padding:11px 20px; border-bottom:1px solid var(--border); cursor:pointer; transition:background .12s; user-select:none; }
.item:last-child { border-bottom:none; }
.item:hover { background:#faf8f5; }
.item.checked { background:var(--green-light); }
.item.hidden { display:none; }
.item:focus-visible { outline:2px solid var(--green); outline-offset:-2px; }
.item-text { font-size:13px; line-height:1.6; color:var(--text); transition:color .15s; }
.item.checked .item-text { color:var(--muted); text-decoration:line-through; text-decoration-color:#aaa; }
/* ── PARENT-CHILD GROUP STRUCTURE ── */
.item-group { border-bottom:1px solid var(--border); }
.item-group:last-child { border-bottom:none; }
.item-group.hidden { display:none; }
.item.item-parent { border-bottom:none; }
.item.item-parent.checked { background:var(--green-light); }
.item.item-parent.indeterminate { background:#f5f3ef; }
.item-children { position:relative; }
.item-children::before {
content:''; position:absolute;
left:28px; top:0; bottom:18px;
width:1.5px; background:var(--tree-line);
border-radius:2px; pointer-events:none;
}
.item.item-child {
padding-left:58px; position:relative;
background:var(--child-bg);
border-bottom:1px solid var(--border);
}
.item.item-child:last-child { border-bottom:none; }
.item.item-child:hover { background:#f7f5f1; }
.item.item-child.checked { background:var(--green-light); }
.item.item-child .item-text { font-size:12.5px; }
.item.item-child::before {
content:''; position:absolute;
left:28px; top:50%; transform:translateY(-50%);
width:16px; height:1.5px;
background:var(--tree-line); pointer-events:none;
}
/* ── EMPTY STATE ── */
.empty-state { display:none; text-align:center; padding:48px 20px; color:var(--muted); font-size:13px; font-style:italic; }
.empty-state.visible { display:block; }
/* ── EXPORT OVERLAY ── */
#exportOverlay { display:none; position:fixed; inset:0; background:rgba(15,39,68,.72); z-index:9999; justify-content:center; align-items:center; flex-direction:column; gap:14px; color:#fff; font-family:'IBM Plex Mono',monospace; font-size:12px; letter-spacing:1.5px; text-transform:uppercase; }
#exportOverlay.visible { display:flex; }
.spinner { width:30px; height:30px; border:3px solid rgba(255,255,255,.25); border-top-color:#fff; border-radius:50%; animation:spin .75s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
/* ── BACKUP MODAL ── */
.overlay { position:fixed; inset:0; background:rgba(0,0,0,.55); display:flex; align-items:center; justify-content:center; z-index:200; padding:16px; }
.modal { background:var(--modal); border:1px solid var(--bd); border-radius:14px; padding:26px; width:560px; max-width:100%; max-height:92vh; overflow-y:auto; box-shadow:0 8px 40px rgba(0,0,0,.18); }
.bak-item { background:var(--surf2); border:1px solid var(--bd); border-radius:10px; padding:11px 13px; display:flex; align-items:center; gap:9px; margin-bottom:7px; transition:background .15s; }
.bak-item:hover { background:var(--surf3); }
.bak-name { font-weight:700; font-size:13px; color:var(--txt-b); margin-bottom:2px; }
.bak-date { font-size:11px; color:var(--txt-d); }
.bak-actions { display:flex; gap:5px; flex-shrink:0; }
.bak-actions button { border:none; border-radius:6px; padding:4px 9px; cursor:pointer; font-size:11px; font-weight:700; transition:opacity .12s,transform .12s; }
.bak-actions button:hover { opacity:.82; }
.bak-actions button:active { transform:scale(.93); }
.lbl { display:block; font-size:10px; color:var(--txt-d); font-weight:700; text-transform:uppercase; letter-spacing:1px; margin-bottom:7px; }
.finput { background:var(--inp); border:1px solid var(--bd-i); border-radius:9px; color:var(--txt); padding:8px 12px; font-size:13px; outline:none; width:100%; font-family:inherit; transition:border-color .18s,box-shadow .18s; }
.finput::placeholder { color:var(--txt-d); }
.finput:focus { border-color:#2d9e6b; box-shadow:0 0 0 3px rgba(45,158,107,.12); }
.mbtn { border:none; border-radius:9px; padding:7px 15px; cursor:pointer; font-size:13px; font-weight:700; transition:opacity .15s,transform .12s; white-space:nowrap; font-family:inherit; }
.mbtn:hover { opacity:.85; }
.mbtn:active { transform:scale(.95); }
.mbtn-ghost { background:var(--ghost-bg); color:var(--ghost-c); border:1px solid var(--ghost-bd); }
.mbtn-green { background:rgba(45,158,107,.12); color:var(--green); border:1px solid rgba(45,158,107,.25); }
/* ── TOAST ── */
#notif { position:fixed; bottom:22px; right:22px; background:var(--navy); border:1px solid var(--navy-light); border-radius:10px; padding:11px 17px; font-size:13px; font-weight:600; color:#fff; z-index:9998; opacity:0; pointer-events:none; transition:opacity .25s,transform .25s cubic-bezier(.34,1.3,.64,1); transform:translateY(12px); font-family:'IBM Plex Sans',sans-serif; }
#notif.show { opacity:1; transform:translateY(0); }
/* ── PRINT ── */
@media print {
* { -webkit-print-color-adjust:exact; print-color-adjust:exact; }
body { background:#fff; padding:0; margin:0; }
header { position:static; box-shadow:none; padding:20px 30px 16px; max-width:100%; width:100%; }
main { max-width:100%; padding:0 30px; margin-top:16px; }
.toolbar,.saved-row,#exportOverlay,.complete-banner,#modal-root,#notif { display:none !important; }
.section { box-shadow:none; border:1px solid #ddd; break-inside:avoid; }
.section.collapsed .section-items { max-height:99999px !important; }
.item,.item.hidden { display:flex !important; }
.item-group.hidden { display:block !important; }
.collapse-icon { display:none; }
.export-timestamp { display:block !important; }
#projectTitle { border-bottom:none; }
#projectTitle:empty::before { display:none; }
.title-sep.hidden { display:inline !important; }
}
</style>
</head>
<body>
<div id="exportOverlay"><div class="spinner"></div><span>Generating image...</span></div>
<div id="modal-root"></div>
<div id="notif"></div>
<header>
<div>
<div class="eyebrow">Plan Presentation Guide · Section 01</div>
<div class="title-row">
<span
id="projectTitle"
contenteditable="plaintext-only"
spellcheck="false"
data-placeholder="Project name"
oninput="onTitleInput()"
onkeydown="if(event.key==='Enter')event.preventDefault()"
onpaste="event.preventDefault();document.execCommand('insertText',false,event.clipboardData.getData('text/plain'))"
></span>
<span class="title-sep hidden" id="titleSep">/</span>
<span class="title-static">Cover & Signature Drawing</span>
</div>
<div class="export-timestamp" id="exportTimestamp"></div>
</div>
<div class="progress-block">
<div class="progress-count"><span id="done">0</span>/<span id="total">0</span></div>
<div class="progress-label">Complete</div>
<div class="progress-bar-outer"><div class="progress-bar-inner" id="bar"></div></div>
</div>
</header>
<div class="toolbar">
<div class="filter-group">
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All</button>
<button class="filter-btn" data-filter="remaining" onclick="setFilter('remaining')">Remaining</button>
<button class="filter-btn" data-filter="done" onclick="setFilter('done')">Done</button>
</div>
<div class="action-group">
<button class="action-btn highlight" onclick="showBackupModal()">Backups</button>
<button class="action-btn" onclick="exportPNG()">Save PNG</button>
<button class="action-btn" onclick="printPDF()">Save PDF</button>
<button class="action-btn danger" onclick="resetAll()">Reset</button>
</div>
</div>
<div class="saved-row">
<div class="saved-indicator" id="savedIndicator"></div>
</div>
<main>
<div class="complete-banner" id="completeBanner">All items complete - great work!</div>
<div id="checklist"></div>
<div class="empty-state" id="emptyState">No items match this filter.</div>
</main>
<script>
var sections = [
// ── 01.001 ──────────────────────────────────────────────────────────────────
{
title: "01.001 — General: Cover Drawing",
items: [
{ text: "A cover drawing is required for project identification and contract purposes. Additional cover drawings may be needed based on engineering discretion.", children: [] }
]
},
// ── 01.002 ──────────────────────────────────────────────────────────────────
{
title: "01.002 — Required Information: Cover Drawing",
items: [
{ text: "Project Owner (i.e.: GDOT or other entity)", children: [] },
{ text: "Project description", children: [] },
{ text: "Project number", children: [] },
{ text: "Project identification number (P.I. NO.)", children: [] },
{ text: "State and Federal route number", children: [] },
{ text: "County name", children: [] },
{ text: "Graphic representation of the project (including centerline and stationing)", children: [] },
{ text: "Congressional district(s) and percentages", children: [] },
{ text: "Standard note referring to GDOT Specifications", children: [] },
{ text: "\"This project contains post construction stormwater (permanent) BMPs.\" note (if applicable)", children: [] },
{ text: "Project location map oriented with north at the top of drawing", children: [] },
{ text: "Begin/End Project Stations to the nearest 100th (i.e. 1010+26.25) (mainline). This station range should encompass all proposed work shown in the plans (permanent construction, temporary/staging construction, ditch grading, signing and pavement marking, etc.). No proposed work (other than temporary signs for traffic control) should be shown outside the Begin/End Project Stations in the plans.", children: [] },
{ text: "Additional \"Begin/End ______\" Station callouts should be shown at the discretion of the Design Phase Leader to clearly describe the limits of project work. Examples may include: Begin/End Full Depth Construction, Begin/End Overlay, Begin/End Temporary Pavement, Begin/End Ditch Grading, etc.", children: [] },
{ text: "Functional classification of mainline", children: [] },
{ text: "Project Length Table", children: [
"County",
"Net Length of Roadway",
"Net Length of Bridges",
"Net Length of Project",
"Net Length of Exceptions",
"Gross Length of Project"
]},
{ text: "Design Traffic Data of mainline and crossroads (as appropriate)", children: [
"Base Year Traffic (two-way) – Traffic A.D.T.",
"Design Year Traffic (two-way) – Traffic A.D.T.",
"Design Hourly Volume (One way, Design year) – Traffic D.H.V.",
"Directional distribution (%)",
"Truck percentages",
"Truck percentages (24 hour)",
"Design speed (MPH)"
]},
{ text: "Revision Summary Table (date and drawing number)", children: [] },
{ text: "Road names and route numbers (including County Road # (CR#) or City Street # (CS#))", children: [] },
{ text: "Project (approximate) midpoint station (to nearest foot) and coordinates (calculated from Begin/End Project Stations)", children: [] },
{ text: "Stations where centerline crosses county boundaries", children: [] },
{ text: "Project designation (e.g.: PODI, Exempt or S.F.)", children: [] },
{ text: "Horizontal and vertical datum used", children: [] },
{ text: "Designation for \"Plans prepared by:\" (Design Engineer, Name only or Consultant Name)", children: [] },
{ text: "Designation for \"Recommended for submission by:\" (Design Engineer Group Manager, District Design Engineer (Design Engineer of Record) – Name only <strong>OMIT FOR CONSULTANT DESIGN PLANS</strong>)", children: [] },
{ text: "Designation for \"Recommended for acceptance by:\" (State Roadway Design Engineer, District Engineer, State Program Delivery Administrator, State Alternative Delivery Administrator, State Express Lanes Administrator signature)", children: [] },
{ text: "GDOT Chief Engineer (Signature and Date)", children: [] },
{ text: "Location & Design approval date", children: [] },
{ text: "Plans completed date (date submitted to Contracts)", children: [] },
{ text: "Applicable CS&D Book note (unless ALL applicable standards and details are included in the plan set and listed in the Index)", children: [
"THESE PLANS HAVE BEEN PREPARED IN ACCORDANCE WITH THE ________ CONSTRUCTION STANDARDS AND DETAILS BOOK AND ATTACHED APPLICABLE REVISIONS. THE _______ CONSTRUCTION STANDARDS AND DETAILS BOOK IS AVAILABLE AT: http://mydocs.dot.ga.gov/info/gdotpubs/ConstructionStandardsAndDetails/Forms/AllItems.aspx ANY REVISIONS CONTAINED WITHIN THIS PLAN SET SUPERSEDE THE ________ CONSTRUCTION STANDARDS AND DETAILS BOOK WHICH THEY REVISE OR IN WHICH THERE IS A CONFLICT.",
"DO NOT DEVIATE FROM THE ABOVE LANGUAGE"
]}
]
},
// ── 01.003 ──────────────────────────────────────────────────────────────────
{
title: "01.003 — Project-Specific Information: Cover Drawing",
items: [
{ text: "Topography <em>(optional — at discretion of Design Phase Leader)</em>", children: [] },
{ text: "Equalities on mainline", children: [] },
{ text: "Begin and end bridge stations (labeled on centerline)", children: [] },
{ text: "Rivers and stream names", children: [] },
{ text: "ESA's <em>(optional — at discretion of Design Phase Leader)</em>", children: [] },
{ text: "State Lines, County Lines, City Limits, Land Lot Lines, GMD", children: [] },
{ text: "Existing property lines <em>(optional — at discretion of Design Phase Leader)</em>", children: [] },
{ text: "Parcel numbers <em>(optional — at discretion of Design Phase Leader)</em>", children: [] },
{ text: "Required right-of-way lines <em>(optional — at discretion of Design Phase Leader)</em>", children: [] },
{ text: "Median locations and sidewalks <em>(optional — at discretion of Design Phase Leader)</em>", children: [] },
{ text: "Stations of Crossroad and Mainline at intersection", children: [] },
{ text: "Railroad lines and owner name", children: [] },
{ text: "Major utility facilities (i.e.: transmission lines, substations, pipelines, etc.)", children: [] }
]
},
// ── 01.004 ──────────────────────────────────────────────────────────────────
{
title: "01.004 — Drawing Layout: Cover Drawing",
items: [
{ text: "Drawing layout for the Cover Drawing conforms to GDOT PPG Section 01.004 requirements.", children: [] }
]
},
// ── 01.005 ──────────────────────────────────────────────────────────────────
{
title: "01.005 — General: Signature Drawing",
items: [
{ text: "Is required for consultant designed projects and is used to define an engineer's area of responsibility for the portions of the plans being digitally signed.", children: [] },
{ text: "Is required for plans that will be signed by a single professional engineer or by more than one professional engineer.", children: [] },
{ text: "Is required on GDOT designed projects if the plan set contains component drawings submitted by an external entity (e.g., Section 44).", children: [] },
{ text: "Is to be located directly behind the last Cover Drawing.", children: [] },
{ text: "Revisions: A separate revision Signature Drawing shall be submitted with the revised drawings and shall be numbered xxA, xxB, etc. Only the professionals required to sign and seal the revision are to be included on the Signature Drawing. The revision Signature Drawing will be placed directly behind the original Signature Drawing(s) or the previous revision Signature Drawing, whichever is applicable.", children: [] }
]
},
// ── 01.006 ──────────────────────────────────────────────────────────────────
{
title: "01.006 — Required Information: Signature Drawing",
items: [
{ text: "For Final Plans only, signature and seal of the professional(s) of record (1.5\" in diameter on the record plan size)", children: [] },
{ text: "Statement of Responsibility (if there are multiple PE's responsible for a drawing to define the licensed professional's limits of responsibility and any required explanatory language)", children: [] },
{ text: "Index (list of drawings that the licensed professional is responsible for signing and sealing)", children: [
"All Sections designed by a consultant (except those addressed in the following bullets) shall include a signature and seal in the Signature Drawing(s) of Section 01.",
"Sections 40, 41, 52, and 56 will not be covered by PE signature and seal. These sections are produced by GDOT. The signature drawing shall include the cell noting this.",
"Section 38 should not be included in the drawing list of the Signature Drawing unless the special details were developed by a consultant.",
"Section 50 requires a signature and seal. See Section 50 for cover drawing signature and seal requirements.",
"See Section 60 in Chapter 3 for separate signature and seal requirements. Section 60 shall have its own Signature Drawing regardless of its inclusion in the Final Plans."
]}
]
},
// ── 01.007 ──────────────────────────────────────────────────────────────────
{
title: "01.007 — Drawing Layout: Signature Drawing",
items: [
{ text: "Signature Drawing layout includes: registered engineer's signature and seal (1.5\" diameter), PE number, engineering firm name and street address, city/state/zip, certificate of authorization number and expiration date, and drawing number (202X.XX.XX format).", children: [] },
{ text: "Complete index of all drawings signed and sealed is included, listing Drawing No. and Drawing Description for each sheet.", children: [] },
{ text: "NOTE: Drawings in Sections 40, 41, 52, and 56 are GDOT Standards and Details and are NOT covered by this signature and seal. Drawings in Section 38 contain GDOT Special Design Details and are not covered by this signature and seal unless otherwise listed.", children: [] }
]
}
];
var STORAGE_KEY = 'sec01_state';
var BACKUP_STORE_KEY = 'sec01_backups';
var EXPORT_FILENAME = 'Section01-Checklist.png';
var MAX_BACKUPS = 25;
var state = {};
var collapsed = {};
var currentFilter = 'all';
var savedAt = null;
var saveTimer = null;
var CHECKMARK = '<svg class="check" width="10" height="8" viewBox="0 0 10 8" fill="none"><path d="M1 3.5L3.8 6.5L9 1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var DASHMARK = '<svg class="dash" width="8" height="2" viewBox="0 0 8 2" fill="none"><path d="M1 1H7" stroke="white" stroke-width="2" stroke-linecap="round"/></svg>';
function getTitle() { return document.getElementById('projectTitle').textContent.trim(); }
function countLeaves(si) {
var n = 0;
sections[si].items.forEach(function(item) {
n += (item.children && item.children.length > 0) ? item.children.length : 1;
});
return n;
}
function countCheckedLeaves(si) {
var n = 0;
sections[si].items.forEach(function(item, pi) {
if (item.children && item.children.length > 0) {
item.children.forEach(function(_, ci) { if (state[si+'-'+pi+'-'+ci]) n++; });
} else {
if (state[si+'-'+pi]) n++;
}
});
return n;
}
function makeCheckbox() {
return '<div class="checkbox">' + CHECKMARK + DASHMARK + '</div>';
}
function buildChecklist() {
var container = document.getElementById('checklist');
var grandTotal = 0;
sections.forEach(function(sec, si) {
collapsed[si] = false;
var leafCount = countLeaves(si);
grandTotal += leafCount;
var wrap = document.createElement('div');
wrap.className = 'section'; wrap.id = 'sec-' + si;
var hdr = document.createElement('div');
hdr.className = 'section-header';
hdr.setAttribute('role','button'); hdr.setAttribute('aria-expanded','true');
hdr.innerHTML =
'<div class="section-header-left"><span class="collapse-icon">▼</span><span class="section-title">' + sec.title + '</span></div>' +
'<span class="section-badge" id="badge-' + si + '">0/' + leafCount + '</span>';
hdr.addEventListener('click', function() { toggleSection(si); });
wrap.appendChild(hdr);
var itemsWrap = document.createElement('div');
itemsWrap.className = 'section-items'; itemsWrap.id = 'items-' + si;
sec.items.forEach(function(item, pi) {
var children = item.children || [];
if (children.length === 0) {
var id = si + '-' + pi;
state[id] = false;
var el = document.createElement('div');
el.className = 'item'; el.id = 'item-' + id;
el.tabIndex = 0; el.setAttribute('role','checkbox'); el.setAttribute('aria-checked','false');
el.innerHTML = makeCheckbox() + '<span class="item-text">' + item.text + '</span>';
(function(s,p){ el.addEventListener('click', function(){ toggleStandalone(s,p); }); el.addEventListener('keydown', function(e){ if(e.key===' '||e.key==='Enter'){e.preventDefault();toggleStandalone(s,p);} }); })(si,pi);
itemsWrap.appendChild(el);
} else {
state[si + '-' + pi] = false;
children.forEach(function(_, ci) { state[si+'-'+pi+'-'+ci] = false; });
var group = document.createElement('div');
group.className = 'item-group'; group.id = 'group-' + si + '-' + pi;
var parentEl = document.createElement('div');
parentEl.className = 'item item-parent'; parentEl.id = 'item-' + si + '-' + pi;
parentEl.tabIndex = 0; parentEl.setAttribute('role','checkbox'); parentEl.setAttribute('aria-checked','false');
parentEl.innerHTML = makeCheckbox() + '<span class="item-text">' + item.text + '</span>';
(function(s,p){ parentEl.addEventListener('click', function(){ toggleParent(s,p); }); parentEl.addEventListener('keydown', function(e){ if(e.key===' '||e.key==='Enter'){e.preventDefault();toggleParent(s,p);} }); })(si,pi);
group.appendChild(parentEl);
var childWrap = document.createElement('div');
childWrap.className = 'item-children'; childWrap.id = 'children-' + si + '-' + pi;
children.forEach(function(childText, ci) {
var cid = si + '-' + pi + '-' + ci;
var cel = document.createElement('div');
cel.className = 'item item-child'; cel.id = 'item-' + cid;
cel.tabIndex = 0; cel.setAttribute('role','checkbox'); cel.setAttribute('aria-checked','false');
cel.innerHTML = makeCheckbox() + '<span class="item-text">' + childText + '</span>';
(function(s,p,c){ cel.addEventListener('click', function(){ toggleChild(s,p,c); }); cel.addEventListener('keydown', function(e){ if(e.key===' '||e.key==='Enter'){e.preventDefault();toggleChild(s,p,c);} }); })(si,pi,ci);
childWrap.appendChild(cel);
});
group.appendChild(childWrap);
itemsWrap.appendChild(group);
}
});
wrap.appendChild(itemsWrap);
container.appendChild(wrap);
});
document.getElementById('total').textContent = grandTotal;
}
function toggleStandalone(si, pi) {
var id = si + '-' + pi;
state[id] = !state[id];
var el = document.getElementById('item-' + id);
el.classList.toggle('checked', state[id]);
el.setAttribute('aria-checked', String(state[id]));
applyFilter(); updateProgress(); scheduleSave();
}
function toggleParent(si, pi) {
var children = sections[si].items[pi].children;
var allChecked = children.every(function(_, ci) { return state[si+'-'+pi+'-'+ci]; });
var newVal = !allChecked;
children.forEach(function(_, ci) {
var cid = si + '-' + pi + '-' + ci;
state[cid] = newVal;
var cel = document.getElementById('item-' + cid);
if (cel) { cel.classList.toggle('checked', newVal); cel.setAttribute('aria-checked', String(newVal)); }
});
state[si + '-' + pi] = newVal;
syncParentVisual(si, pi);
applyFilter(); updateProgress(); scheduleSave();
}
function toggleChild(si, pi, ci) {
var cid = si + '-' + pi + '-' + ci;
state[cid] = !state[cid];
var cel = document.getElementById('item-' + cid);
if (cel) { cel.classList.toggle('checked', state[cid]); cel.setAttribute('aria-checked', String(state[cid])); }
var children = sections[si].items[pi].children;
var checkedCount = children.filter(function(_, i) { return state[si+'-'+pi+'-'+i]; }).length;
state[si + '-' + pi] = (checkedCount === children.length);
syncParentVisual(si, pi);
applyFilter(); updateProgress(); scheduleSave();
}
function syncParentVisual(si, pi) {
var children = sections[si].items[pi].children;
var checkedCount = children.filter(function(_, ci) { return state[si+'-'+pi+'-'+ci]; }).length;
var el = document.getElementById('item-' + si + '-' + pi);
if (!el) return;
var allChecked = checkedCount === children.length;
var someChecked = checkedCount > 0 && !allChecked;
el.classList.toggle('checked', allChecked);
el.classList.toggle('indeterminate', someChecked);
el.setAttribute('aria-checked', allChecked ? 'true' : someChecked ? 'mixed' : 'false');
}
function syncAllParentVisuals() {
sections.forEach(function(sec, si) {
sec.items.forEach(function(item, pi) {
if (item.children && item.children.length > 0) syncParentVisual(si, pi);
});
});
}
function toggleSection(si) {
collapsed[si] = !collapsed[si];
var wrap = document.getElementById('sec-' + si);
wrap.classList.toggle('collapsed', collapsed[si]);
wrap.querySelector('.section-header').setAttribute('aria-expanded', String(!collapsed[si]));
}
function onTitleInput() {
document.getElementById('titleSep').classList.toggle('hidden', !getTitle());
scheduleSave();
}
function setFilter(f) {
currentFilter = f;
document.querySelectorAll('.filter-btn').forEach(function(b){ b.classList.toggle('active', b.dataset.filter===f); });
applyFilter();
}
function applyFilter() {
var anyVisible = false;
sections.forEach(function(sec, si) {
sec.items.forEach(function(item, pi) {
var children = item.children || [];
if (children.length === 0) {
var checked = state[si+'-'+pi];
var show = currentFilter==='all' ? true : currentFilter==='done' ? checked : !checked;
var el = document.getElementById('item-' + si + '-' + pi);
if (el) el.classList.toggle('hidden', !show);
if (show) anyVisible = true;
} else {
var anyChildVisible = false;
children.forEach(function(_, ci) {
var checked = state[si+'-'+pi+'-'+ci];
var show = currentFilter==='all' ? true : currentFilter==='done' ? checked : !checked;
var cel = document.getElementById('item-' + si + '-' + pi + '-' + ci);
if (cel) cel.classList.toggle('hidden', !show);
if (show) anyChildVisible = true;
});
var group = document.getElementById('group-' + si + '-' + pi);
if (group) group.classList.toggle('hidden', !anyChildVisible);
if (anyChildVisible) anyVisible = true;
}
});
});
document.getElementById('emptyState').classList.toggle('visible', !anyVisible);
}
function updateProgress() {
var grandTotal = 0, grandDone = 0;
sections.forEach(function(sec, si) {
var secTotal = countLeaves(si);
var secDone = countCheckedLeaves(si);
grandTotal += secTotal;
grandDone += secDone;
var badge = document.getElementById('badge-' + si);
if (badge) { badge.textContent = secDone + '/' + secTotal; badge.classList.toggle('complete', secDone === secTotal); }
});
document.getElementById('done').textContent = grandDone;
document.getElementById('total').textContent = grandTotal;
document.getElementById('bar').style.width = grandTotal ? ((grandDone / grandTotal) * 100) + '%' : '0%';
document.getElementById('completeBanner').classList.toggle('visible', grandDone === grandTotal && grandTotal > 0);
}
function resetAll() {
if (!confirm('Reset all checked items and clear the project name?')) return;
Object.keys(state).forEach(function(id) {
state[id] = false;
var el = document.getElementById('item-' + id);
if (el) { el.classList.remove('checked','indeterminate'); el.setAttribute('aria-checked','false'); }
});
document.getElementById('projectTitle').textContent = '';
document.getElementById('titleSep').classList.add('hidden');
applyFilter(); updateProgress(); scheduleSave();
}
function buildPayload() {
return { checked: Object.assign({}, state), title: getTitle(), savedAt: Date.now() };
}
function applyData(data) {
if (!data) return;
if (data.checked) {
Object.keys(data.checked).forEach(function(id) {
if (!(id in state)) return;
state[id] = data.checked[id];
var el = document.getElementById('item-' + id);
if (el) { el.classList.toggle('checked', state[id]); el.setAttribute('aria-checked', String(state[id])); }
});
}
if (data.title) {
document.getElementById('projectTitle').textContent = data.title;
document.getElementById('titleSep').classList.toggle('hidden', !data.title.trim());
}
if (data.savedAt) savedAt = data.savedAt;
}
function scheduleSave() { clearTimeout(saveTimer); saveTimer = setTimeout(doSave, 600); }
function doSave() {
var payload = buildPayload();
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(payload)); savedAt = payload.savedAt; flashSaved(); }
catch(e) { console.warn('Save failed:', e); }
}
function loadSaved() {
try { var raw = localStorage.getItem(STORAGE_KEY); if (raw) applyData(JSON.parse(raw)); } catch(e) {}
syncAllParentVisuals();
applyFilter(); updateProgress(); updateSavedLabel();
}
function flashSaved() {
var el = document.getElementById('savedIndicator');
el.textContent = 'Saved'; el.classList.add('flash');
setTimeout(function(){ el.classList.remove('flash'); updateSavedLabel(); }, 1800);
}
function updateSavedLabel() {
var el = document.getElementById('savedIndicator');
if (!savedAt) { el.textContent = ''; return; }
var s = Math.floor((Date.now()-savedAt)/1000);
if (s < 15) el.textContent = 'Saved just now';
else if (s < 3600) el.textContent = 'Saved ' + (Math.floor(s/60)||1) + 'm ago';
else el.textContent = 'Saved at ' + new Date(savedAt).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});
}
setInterval(updateSavedLabel, 60000);
function uid() { return '_' + Math.random().toString(36).slice(2,9); }
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
function getBaks() { try { return JSON.parse(localStorage.getItem(BACKUP_STORE_KEY)||'[]'); } catch(e){ return []; } }
function setBaks(a) { localStorage.setItem(BACKUP_STORE_KEY, JSON.stringify(a)); }
function createBak(name) {
var a = getBaks();
var done = Object.values(state).filter(Boolean).length;
a.unshift({ id:uid(), name:name||'Snapshot '+new Date().toLocaleString(), ts:Date.now(), checked:done, total:Object.keys(state).length, data:JSON.stringify(buildPayload()) });
if (a.length > MAX_BACKUPS) a.splice(MAX_BACKUPS);
setBaks(a); notify('Snapshot saved!'); showBackupModal();
}
function restoreBak(id) { var b=getBaks().find(function(x){return x.id===id;}); if(b) showRestoreConfirm(id,b.name); }
function doRestoreBak(id) {
var b=getBaks().find(function(x){return x.id===id;}); if(!b) return;
Object.keys(state).forEach(function(k){state[k]=false;});
applyData(JSON.parse(b.data));
localStorage.setItem(STORAGE_KEY, b.data);
syncAllParentVisuals(); applyFilter(); updateProgress();
closeModal(); notify('Checklist restored!');
}
function deleteBak(id) { setBaks(getBaks().filter(function(x){return x.id!==id;})); showBackupModal(); }
function exportBak(id) {
var raw, name;
if (id) { var b=getBaks().find(function(x){return x.id===id;}); if(!b)return; raw=b.data; name=b.name; }
else { raw=JSON.stringify(buildPayload(),null,2); name=STORAGE_KEY+'-export'; }
var a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([raw],{type:'application/json'}));
a.download=name.replace(/[^\w\s-]/g,'').trim().replace(/\s+/g,'_')+'_'+Date.now()+'.json'; a.click();
}
function importBak() {
var inp=document.createElement('input'); inp.type='file'; inp.accept='.json';
inp.onchange=function(e){
var f=e.target.files[0]; if(!f)return;
var r=new FileReader();
r.onload=function(ev){
try{ var d=JSON.parse(ev.target.result); if(!d.checked)throw new Error(); showImportConfirm(d); }
catch(err){ showModalBanner('error','Invalid file — not a recognised checklist backup.'); }
};
r.readAsText(f);
};
inp.click();
}
function doImportBak(data) {
Object.keys(state).forEach(function(k){state[k]=false;});
applyData(data); localStorage.setItem(STORAGE_KEY,JSON.stringify(data));
syncAllParentVisuals(); applyFilter(); updateProgress();
closeModal(); notify('Import successful!');
}
function closeModal() { document.getElementById('modal-root').innerHTML=''; }
function setModal(html, onReady) {
var root=document.getElementById('modal-root'); root.innerHTML='';
var ov=document.createElement('div'); ov.className='overlay'; ov.onclick=closeModal;
var m=document.createElement('div'); m.className='modal';
m.onclick=function(e){e.stopPropagation();}; m.innerHTML=html;
ov.appendChild(m); root.appendChild(ov);
if(onReady) setTimeout(onReady,10);
}
function showModalBanner(type,message){
var existing=document.getElementById('modal-banner'); if(existing)existing.remove();
var m=document.querySelector('#modal-root .modal'); if(!m)return;
var colors={warn:{bg:'rgba(245,158,11,.13)',bd:'rgba(245,158,11,.4)',txt:'#b07d1a'},error:{bg:'rgba(200,50,50,.10)',bd:'rgba(200,50,50,.30)',txt:'#b83232'},success:{bg:'rgba(45,158,107,.10)',bd:'rgba(45,158,107,.30)',txt:'#2d9e6b'}};
var c=colors[type]||colors.warn;
var b=document.createElement('div'); b.id='modal-banner';
b.style.cssText='background:'+c.bg+';border:1px solid '+c.bd+';border-radius:10px;padding:11px 14px;margin-bottom:16px;font-size:13px;font-weight:600;color:'+c.txt;
b.textContent=message; m.insertBefore(b,m.firstChild); m.scrollTop=0;
}
function showRestoreConfirm(id,bakName){
var existing=document.getElementById('modal-banner'); if(existing)existing.remove();
var m=document.querySelector('#modal-root .modal'); if(!m)return;
var b=document.createElement('div'); b.id='modal-banner';
b.style.cssText='background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.35);border-radius:10px;padding:12px 14px;margin-bottom:16px';
b.innerHTML='<div style="font-weight:700;font-size:13px;color:#b07d1a;margin-bottom:4px">Restore "'+esc(bakName)+'"?</div><div style="font-size:12px;color:var(--txt-m);margin-bottom:10px">Your current state will be replaced. This cannot be undone.</div><div style="display:flex;gap:8px"><button onclick="doRestoreBak(\''+id+'\')" style="flex:1;border:none;border-radius:7px;padding:7px 0;background:#2d9e6b;color:#fff;font-weight:700;cursor:pointer;font-size:12px;font-family:inherit">Restore</button><button onclick="document.getElementById(\'modal-banner\').remove()" style="flex:1;border:none;border-radius:7px;padding:7px 0;background:var(--surf3);color:var(--txt-m);font-weight:700;cursor:pointer;font-size:12px;font-family:inherit">Cancel</button></div>';
m.insertBefore(b,m.firstChild); m.scrollTop=0;
}
function showImportConfirm(data){
var existing=document.getElementById('modal-banner'); if(existing)existing.remove();
var m=document.querySelector('#modal-root .modal'); if(!m)return;
var b=document.createElement('div'); b.id='modal-banner';
b.style.cssText='background:rgba(245,158,11,.1);border:1px solid rgba(245,158,11,.35);border-radius:10px;padding:12px 14px;margin-bottom:16px';
b.innerHTML='<div style="font-weight:700;font-size:13px;color:#b07d1a;margin-bottom:4px">Import this file?</div><div style="font-size:12px;color:var(--txt-m);margin-bottom:10px">Your current state will be replaced.</div><div style="display:flex;gap:8px"><button id="import-yes" style="flex:1;border:none;border-radius:7px;padding:7px 0;background:#2d9e6b;color:#fff;font-weight:700;cursor:pointer;font-size:12px;font-family:inherit">Yes, Import</button><button onclick="document.getElementById(\'modal-banner\').remove()" style="flex:1;border:none;border-radius:7px;padding:7px 0;background:var(--surf3);color:var(--txt-m);font-weight:700;cursor:pointer;font-size:12px;font-family:inherit">Cancel</button></div>';
m.insertBefore(b,m.firstChild); m.scrollTop=0;
document.getElementById('import-yes').onclick=function(){ doImportBak(data); };
}
function showBackupModal() {
var baks=getBaks();
var bakHtml=baks.length===0
?'<div style="color:var(--txt-dd);font-size:13px;padding:16px 0 8px;font-style:italic">No snapshots yet.</div>'
:baks.map(function(b){
return '<div class="bak-item"><div style="flex:1"><div class="bak-name">'+esc(b.name)+'</div><div class="bak-date">'+new Date(b.ts).toLocaleString()+' · '+b.checked+'/'+b.total+' states checked</div></div><div class="bak-actions"><button onclick="restoreBak(\''+b.id+'\')" style="background:rgba(45,158,107,.15);color:#2d9e6b">Restore</button><button onclick="exportBak(\''+b.id+'\')" style="background:var(--surf3);color:var(--txt-m)">Export</button><button onclick="deleteBak(\''+b.id+'\')" style="background:rgba(184,50,50,.1);color:#b83232">Delete</button></div></div>';
}).join('');
setModal(
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px"><div style="font-size:17px;font-weight:800;color:var(--txt-b)">Backup & Restore</div><button class="mbtn mbtn-ghost" style="padding:4px 10px;font-size:12px" onclick="closeModal()">✕</button></div>'+
'<div style="background:rgba(45,158,107,.08);border:1px solid rgba(45,158,107,.2);border-radius:10px;padding:11px 14px;margin-bottom:20px;font-size:13px;color:var(--green)">Auto-save is ON — every change saves to this browser automatically.</div>'+
'<label class="lbl">Save Snapshot</label>'+
'<div style="display:flex;gap:9px;margin-bottom:20px"><input id="bak-n" class="finput" placeholder="Snapshot name..." style="flex:1" onkeydown="if(event.key===\'Enter\')createBak(this.value)"><button class="mbtn mbtn-green" onclick="createBak(document.getElementById(\'bak-n\').value)">Save</button></div>'+
'<label class="lbl">Transfer</label>'+
'<div style="display:flex;gap:9px;margin-bottom:22px"><button class="mbtn mbtn-ghost" style="flex:1;font-size:12px" onclick="exportBak(null)">Export .json</button><button class="mbtn mbtn-ghost" style="flex:1;font-size:12px" onclick="importBak()">Import .json</button></div>'+
'<label class="lbl">Snapshots ('+baks.length+'/'+MAX_BACKUPS+')</label>'+
'<div style="max-height:260px;overflow-y:auto;padding-right:2px">'+bakHtml+'</div>'+
'<div style="margin-top:16px;padding:12px 14px;background:var(--surf2);border-radius:10px;font-size:12px;color:var(--txt-d);line-height:1.9"><strong style="color:var(--txt-m)">Transferring to another computer:</strong><br>1. Export .json → save to USB or send via email<br>2. On the other machine: open this file → Backups → Import .json</div>',
function(){ document.getElementById('bak-n').focus(); }
);
}
function notify(msg) {
var el=document.getElementById('notif'); el.textContent=msg; el.classList.add('show');
setTimeout(function(){ el.classList.remove('show'); }, 2600);
}
function fmtTimestamp() {
return 'Exported '+new Date().toLocaleString([],{month:'short',day:'numeric',year:'numeric',hour:'2-digit',minute:'2-digit'});
}
async function exportPNG() {
var overlay=document.getElementById('exportOverlay'); overlay.classList.add('visible');
var W=900;
try {
var canvas=await html2canvas(document.body,{
scale:2,useCORS:true,logging:false,width:W,windowWidth:W,scrollX:0,scrollY:0,
onclone:function(doc){
doc.getElementById('exportTimestamp').textContent=fmtTimestamp();
doc.getElementById('exportTimestamp').style.display='block';
doc.getElementById('exportOverlay').style.display='none';
doc.getElementById('modal-root').innerHTML='';
doc.body.style.width=W+'px'; doc.body.style.padding='0 0 40px';
var hdr=doc.querySelector('header');
hdr.style.position='static'; hdr.style.width='100%'; hdr.style.boxShadow='none'; hdr.style.padding='22px 30px 18px';
var mainEl=doc.querySelector('main');
mainEl.style.maxWidth='100%'; mainEl.style.marginTop='18px'; mainEl.style.padding='0 30px';
['toolbar','saved-row'].forEach(function(cls){ var el=doc.querySelector('.'+cls); if(el) el.style.display='none'; });
var banner=doc.getElementById('completeBanner'); if(banner) banner.style.display='none';
doc.querySelectorAll('.section').forEach(function(s){ s.classList.remove('collapsed'); var iw=s.querySelector('.section-items'); if(iw) iw.style.maxHeight='99999px'; });
doc.querySelectorAll('.item,.item-group').forEach(function(el){ el.style.display=el.classList.contains('item-group')?'block':'flex'; });
var empty=doc.getElementById('emptyState'); if(empty) empty.style.display='none';
var sep=doc.getElementById('titleSep'), pt=doc.getElementById('projectTitle');
if(sep && pt && !pt.textContent.trim()) sep.style.display='none';
}
});
var link=document.createElement('a'); link.download=EXPORT_FILENAME; link.href=canvas.toDataURL('image/png'); link.click();
} catch(e){ alert('PNG export failed.'); console.error(e); }
finally { overlay.classList.remove('visible'); }
}
function printPDF() {
var ts=document.getElementById('exportTimestamp'); ts.textContent=fmtTimestamp();
window.print(); setTimeout(function(){ ts.textContent=''; }, 2000);
}
document.addEventListener('keydown', function(e) {
if ((e.ctrlKey||e.metaKey) && e.key==='s') { e.preventDefault(); createBak('Quick Save '+new Date().toLocaleTimeString()); }
if (e.key==='Escape') closeModal();
});
buildChecklist();
loadSaved();
</script>
</body>
</html>