Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_wrapper.html

O Operator
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/TEMPLATES/abforge_wrapper.html

added on local at 2026-07-11 08:13:43

Added
+986
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to fe9a17d6357f
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1<!DOCTYPE html>
2<html lang="en" class="$html_class">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>$title &middot; $website_title</title>
7$meta_tags
8 <link rel="icon" type="image/svg+xml" href="$assets/images/favicon.svg?v=20260608">
9 <link rel="alternate icon" href="$assets/images/favicon.svg?v=20260608">
10 <link rel="apple-touch-icon" href="$assets/images/favicon.svg?v=20260608">
11 <link rel="preconnect" href="https://fonts.googleapis.com">
12 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13 <link href="https://fonts.googleapis.com/css2?family=Inter:wght\@400;500;600;700&family=Space+Grotesk:wght\@500;600;700&family=JetBrains+Mono:wght\@400;600&display=swap" rel="stylesheet">
14 <link rel="stylesheet" href="$assets_css/site.css?v=2026-07-02-padfix">
15 <style>
16 /* Sidebar collapse behavior — instead of hiding the sidebar
17 entirely, it shrinks to a narrow icon-rail showing only the
18 icons (TaskForge / ShopCart pattern). Hamburger lives in the
19 topbar, not floating on the sidebar edge. */
20 :root { --sidebar-rail-w: 48px; }
21 .app { transition: grid-template-columns 0.22s ease; }
22 .sidebar { transition: padding 0.22s ease; overflow-x: hidden; }
23 html.sidebar-collapsed .app { grid-template-columns: var(--sidebar-rail-w) 1fr; }
24
25 /* When collapsed: hide all text, center icons, shrink horizontal padding. */
26 html.sidebar-collapsed .sidebar { padding-left: 0; padding-right: 0; }
27 html.sidebar-collapsed .sidebar-brand { justify-content: center; padding-left: 4px; padding-right: 4px; }
28 html.sidebar-collapsed .sidebar-brand .brand-mark { width: 34px; height: 34px; }
29 html.sidebar-collapsed .sidebar-brand .brand-mark svg { width: 22px; height: 28px; }
30 html.sidebar-collapsed .brand-text,
31 html.sidebar-collapsed .sidebar-link span,
32 html.sidebar-collapsed .sidebar-link .badge,
33 html.sidebar-collapsed .user-meta,
34 html.sidebar-collapsed .sidebar-foot .icon-btn { display: none; }
35 html.sidebar-collapsed .sidebar-nav { padding-left: 6px; padding-right: 6px; }
36 html.sidebar-collapsed .sidebar-link {
37 justify-content: center;
38 padding: 11px 0;
39 gap: 0;
40 }
41 html.sidebar-collapsed .sidebar-link svg { width: 20px; height: 20px; }
42 /* Keep the active accent bar visible but flush against the rail's left edge. */
43 html.sidebar-collapsed .sidebar-link.active::before { left: -6px; }
44 /* Section dividers — replace text labels with thin brand-tinted lines. */
45 html.sidebar-collapsed .sidebar-section-label {
46 display: block !important;
47 visibility: hidden;
48 height: 1px;
49 padding: 0;
50 margin: 12px 14px 6px;
51 background: rgba(255,122,61,0.18);
52 }
53 html.sidebar-collapsed .sidebar-foot {
54 flex-direction: column;
55 padding: 12px 6px;
56 gap: 8px;
57 }
58 html.sidebar-collapsed .sidebar-foot .user-chip { justify-content: center; }
59 html.sidebar-collapsed .user-avatar { width: 32px; height: 32px; font-size: 12px; }
60
61 /* Tooltip on hover when collapsed — uses each link's title attr. */
62 html.sidebar-collapsed .sidebar-link { position: relative; }
63 html.sidebar-collapsed .sidebar-link[title]:hover::after {
64 content: attr(title);
65 position: absolute;
66 left: calc(100% + 14px);
67 top: 50%;
68 transform: translateY(-50%);
69 padding: 6px 10px;
70 background: var(--col-surface-3);
71 border: 1px solid var(--col-border-2);
72 color: var(--col-text);
73 border-radius: 6px;
74 font-size: 12px;
75 font-weight: 600;
76 white-space: nowrap;
77 box-shadow: 0 4px 14px rgba(0,0,0,0.4);
78 z-index: 200;
79 pointer-events: none;
80 }
81
82 /* When collapsed the content area is wider — drop the centered cap. */
83 html.sidebar-collapsed .app-content { max-width: none; padding-left: 24px; padding-right: 24px; }
84 html.sidebar-collapsed .topbar { padding-left: 24px; padding-right: 24px; }
85
86 /* Topbar-only mini brand — only visible when sidebar collapsed. */
87 .topbar-brand {
88 display: none;
89 align-items: center;
90 gap: 10px;
91 text-decoration: none;
92 color: inherit;
93 margin-right: 14px;
94 }
95 .topbar-brand-name {
96 font-family: var(--font-display, inherit);
97 font-size: 15px;
98 font-weight: 700;
99 color: #fff;
100 letter-spacing: 0.3px;
101 }
102
103 /* Hamburger toggle — lives in the topbar (ABForge pattern).
104 Dark-dark-orange fill with a thin white border so it pairs
105 visually with the brand-mark in the sidebar. Brand-orange
106 glyph + soft glow keep it on-brand without screaming. */
107 .sidebar-toggle {
108 /* Ghost button at rest: transparent, borderless, just the icon.
109 Only the chip treatment (dark-orange fill + white border +
110 brand glow) appears on hover. */
111 flex-shrink: 0;
112 width: 38px; height: 38px;
113 background: transparent;
114 border: 1px solid transparent;
115 border-radius: 10px;
116 color: var(--col-accent-bright);
117 display: inline-flex; align-items: center; justify-content: center;
118 cursor: pointer;
119 margin-right: 16px;
120 transition: background 0.15s, color 0.15s, border-color 0.15s, box-shadow 0.15s, transform 0.12s;
121 }
122 .sidebar-toggle:hover {
123 background: transparent;
124 border-color: var(--col-accent);
125 color: #fff;
126 box-shadow: 0 0 12px rgba(255,122,61,0.30);
127 }
128 .sidebar-toggle:active { transform: translateY(1px); }
129 .sidebar-toggle svg { width: 18px; height: 18px; }
130
131 /* Admin chat pill — orange (brand) idle, green (active) when queue
132 has waiting visitors. */
133 .chat-pill {
134 position: relative;
135 display: inline-flex;
136 align-items: center;
137 gap: 9px;
138 padding: 0 14px;
139 height: 36px;
140 border-radius: 10px;
141 font-size: 12px;
142 font-weight: 700;
143 letter-spacing: 0.5px;
144 text-transform: uppercase;
145 text-decoration: none;
146 transition: background 0.18s, color 0.18s, transform 0.12s, box-shadow 0.18s, border-color 0.18s;
147 }
148 .chat-pill .cw-count { font-weight: 800; color: #fff; line-height: 1; }
149 .chat-pill .cw-badge {
150 display: inline-flex;
151 align-items: center;
152 color: #fff;
153 font-size: 12px;
154 font-weight: 800;
155 line-height: 1;
156 margin-left: 4px;
157 }
158 .chat-pill .cw-badge.cw-badge-empty { display: none; }
159 .cw-ping {
160 position: relative;
161 width: 9px; height: 9px;
162 border-radius: 50%;
163 flex-shrink: 0;
164 transition: background 0.18s, box-shadow 0.18s;
165 }
166 .cw-ping::before,
167 .cw-ping::after {
168 content: '';
169 position: absolute;
170 inset: 0;
171 border-radius: 50%;
172 opacity: 0;
173 pointer-events: none;
174 }
175 /* --- Idle: brand orange, no animation ------------------------ */
176 .chat-pill.cw-idle {
177 background: linear-gradient(135deg, rgba(255,122,61,0.18) 0%, rgba(255,179,71,0.12) 100%);
178 border: 1px solid rgba(255,122,61,0.45);
179 color: var(--col-accent-bright);
180 box-shadow: 0 0 10px rgba(255,122,61,0.18);
181 }
182 .chat-pill.cw-idle:hover {
183 background: linear-gradient(135deg, rgba(255,122,61,0.28) 0%, rgba(255,179,71,0.20) 100%);
184 color: #fff;
185 transform: translateY(-1px);
186 box-shadow: 0 0 16px rgba(255,122,61,0.45);
187 }
188 .chat-pill.cw-idle:focus-visible {
189 outline: 2px solid var(--col-accent);
190 outline-offset: 2px;
191 }
192 .chat-pill.cw-idle .cw-ping {
193 background: var(--col-accent);
194 box-shadow: 0 0 6px rgba(255,122,61,0.7);
195 }
196
197 /* --- Active: green pulsing dot + count badge ------------------ */
198 .chat-pill.cw-active {
199 background: linear-gradient(135deg, rgba(16,185,129,0.28) 0%, rgba(16,185,129,0.16) 100%);
200 border: 1px solid rgba(16,185,129,0.70);
201 color: #fff;
202 box-shadow: 0 0 18px rgba(16,185,129,0.45);
203 transition: opacity 0.45s ease-in-out, background 0.18s, color 0.18s, transform 0.12s, box-shadow 0.18s, border-color 0.18s;
204 }
205 .chat-pill.cw-active.cw-blink-off { opacity: 0.65; }
206 .chat-pill.cw-active:hover {
207 background: linear-gradient(135deg, rgba(16,185,129,0.34) 0%, rgba(16,185,129,0.20) 100%);
208 color: #fff;
209 transform: translateY(-1px);
210 box-shadow: 0 0 22px rgba(16,185,129,0.65);
211 }
212 .chat-pill.cw-active:focus-visible {
213 outline: 2px solid #34d399;
214 outline-offset: 2px;
215 }
216 .chat-pill.cw-active .cw-ping {
217 background: #10b981;
218 box-shadow: 0 0 8px rgba(16,185,129,0.9);
219 }
220 .chat-pill.cw-active .cw-ping::before,
221 .chat-pill.cw-active .cw-ping::after {
222 background: #10b981;
223 animation: cw-ping 1.8s cubic-bezier(0.0, 0.0, 0.2, 1) infinite;
224 }
225 .chat-pill.cw-active .cw-ping::after { animation-delay: 0.9s; }
226
227 @keyframes cw-ping {
228 0% { transform: scale(1); opacity: 0.7; }
229 80% { opacity: 0; }
230 100% { transform: scale(3.6); opacity: 0; }
231 }
232 @keyframes cw-box-blink {
233 0%, 100% { opacity: 1; }
234 50% { opacity: 0.40; }
235 }
236 .chat-pill.cw-active.cw-bump { animation: cw-bump 0.45s cubic-bezier(0.34, 1.56, 0.64, 1), cw-box-blink 2.0s ease-in-out infinite; }
237 @keyframes cw-bump {
238 0% { transform: scale(1); }
239 50% { transform: scale(1.16); }
240 100% { transform: scale(1); }
241 }
242
243 /* Messages pill — mirrors chat-pill idle style in brand orange. */
244 .msg-pill {
245 position: relative;
246 display: inline-flex;
247 align-items: center;
248 gap: 9px;
249 padding: 0 14px;
250 height: 36px;
251 border-radius: 10px;
252 font-size: 12px;
253 font-weight: 700;
254 letter-spacing: 0.5px;
255 text-transform: uppercase;
256 text-decoration: none;
257 background: linear-gradient(135deg, rgba(255,122,61,0.18) 0%, rgba(255,179,71,0.12) 100%);
258 border: 1px solid rgba(255,122,61,0.45);
259 color: var(--col-accent-bright);
260 box-shadow: 0 0 10px rgba(255,122,61,0.18);
261 transition: opacity 0.45s ease-in-out, background 0.18s, color 0.18s, transform 0.12s, box-shadow 0.18s, border-color 0.18s;
262 }
263 .msg-pill.has-unread.msg-blink-off { opacity: 0.65; }
264 .msg-pill .msg-badge.msg-badge-empty { display: none; }
265 .msg-pill:hover {
266 background: linear-gradient(135deg, rgba(255,122,61,0.28) 0%, rgba(255,179,71,0.20) 100%);
267 color: #fff;
268 transform: translateY(-1px);
269 box-shadow: 0 0 16px rgba(255,122,61,0.45);
270 }
271 .msg-pill:focus-visible {
272 outline: 2px solid var(--col-accent);
273 outline-offset: 2px;
274 }
275 .msg-pill .msg-dot {
276 width: 9px; height: 9px;
277 border-radius: 50%;
278 background: var(--col-accent);
279 box-shadow: 0 0 6px rgba(255,122,61,0.7);
280 flex-shrink: 0;
281 }
282 .msg-pill .msg-badge {
283 display: inline-flex;
284 align-items: center;
285 justify-content: center;
286 min-width: 18px; height: 18px;
287 padding: 0 6px;
288 border-radius: 9px;
289 background: #ef4444;
290 color: #fff;
291 font-size: 11px;
292 font-weight: 800;
293 line-height: 1;
294 box-shadow: 0 0 8px rgba(239,68,68,0.55);
295 margin-left: 2px;
296 }
297
298 /* Topbar Help button — brand-orange chip + dropdown. */
299 .help-btn {
300 display: inline-flex;
301 align-items: center;
302 gap: 7px;
303 height: 36px;
304 padding: 0 14px;
305 border-radius: 10px;
306 background: linear-gradient(135deg, rgba(255,122,61,0.22) 0%, rgba(255,179,71,0.18) 100%);
307 border: 1px solid rgba(255,122,61,0.55);
308 color: var(--col-accent-bright);
309 font-size: 13px;
310 font-weight: 700;
311 letter-spacing: 0.3px;
312 text-decoration: none;
313 box-shadow: 0 0 14px rgba(255,122,61,0.32);
314 transition: background 0.15s, color 0.15s, transform 0.12s, box-shadow 0.15s;
315 }
316 .help-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
317 .help-btn:hover {
318 background: linear-gradient(135deg, rgba(255,122,61,0.32) 0%, rgba(255,179,71,0.28) 100%);
319 color: #fff;
320 transform: translateY(-1px);
321 box-shadow: 0 0 18px rgba(255,122,61,0.55);
322 }
323 .help-btn:focus-visible {
324 outline: 2px solid var(--col-accent);
325 outline-offset: 2px;
326 }
327 .help-btn-chevron {
328 width: 12px !important; height: 12px !important;
329 opacity: 0.85;
330 transition: transform 0.15s, opacity 0.15s;
331 }
332 .help-menu:hover .help-btn-chevron,
333 .help-menu:focus-within .help-btn-chevron {
334 transform: rotate(180deg);
335 opacity: 1;
336 }
337
338 .help-menu { position: relative; }
339 .help-dropdown {
340 position: absolute;
341 top: calc(100% + 8px);
342 right: 0;
343 min-width: 240px;
344 background: var(--col-surface-2);
345 border: 1px solid var(--col-border);
346 border-radius: 12px;
347 padding: 6px 0 8px;
348 box-shadow: 0 20px 60px rgba(0,0,0,0.5);
349 opacity: 0; visibility: hidden;
350 transform: translateY(-6px);
351 transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
352 z-index: 200;
353 }
354 .help-dropdown::before {
355 content: ""; position: absolute;
356 top: -8px; left: 0; right: 0; height: 8px;
357 }
358 .help-menu:hover .help-dropdown,
359 .help-menu:focus-within .help-dropdown {
360 opacity: 1; visibility: visible; transform: translateY(0);
361 }
362 .help-dropdown-section {
363 font-size: 9px;
364 font-weight: 800;
365 letter-spacing: 1.5px;
366 text-transform: uppercase;
367 color: var(--col-text-3);
368 padding: 10px 16px 4px;
369 }
370 .help-dropdown a {
371 display: flex; align-items: center; gap: 10px;
372 padding: 9px 16px;
373 font-size: 13px; font-weight: 600;
374 color: var(--col-text-2);
375 text-decoration: none;
376 transition: color 0.12s, background 0.12s;
377 }
378 .help-dropdown a svg {
379 width: 14px; height: 14px;
380 flex-shrink: 0;
381 color: var(--col-text-3);
382 }
383 .help-dropdown a:hover,
384 .help-dropdown a:focus-visible {
385 color: var(--col-accent-bright);
386 background: var(--col-surface-3);
387 outline: none;
388 }
389 .help-dropdown a:hover svg,
390 .help-dropdown a:focus-visible svg {
391 color: var(--col-accent-bright);
392 }
393
394 /* Impersonation banner -- only rendered while an admin is acting
395 as another user. */
396 .impersonation-banner {
397 display: flex; align-items: center; justify-content: space-between;
398 gap: 14px; flex-wrap: wrap;
399 padding: 10px 18px;
400 background: linear-gradient(90deg, rgba(245,158,11,0.20), rgba(239,68,68,0.20));
401 border-bottom: 1px solid rgba(245,158,11,0.45);
402 color: #fde68a; font-size: 13px;
403 }
404 .impersonation-banner-msg { display:flex; align-items:center; gap:10px; }
405 .impersonation-banner-msg svg { flex-shrink: 0; }
406 .impersonation-banner-msg strong { color: #fff; }
407 .impersonation-banner-form { margin: 0; }
408 .impersonation-banner-stop {
409 background: rgba(239,68,68,0.85);
410 border: 1px solid rgba(239,68,68,1);
411 color: #fff; font-weight: 700;
412 padding: 6px 14px; border-radius: 8px;
413 font-size: 12px; cursor: pointer;
414 letter-spacing: 0.5px;
415 }
416 .impersonation-banner-stop:hover { background: #dc2626; }
417
418 /* Top-right profile menu — hover-reveal dropdown. */
419 .profile-menu { position: relative; flex-shrink: 0; }
420
421 /* ── Inline trigger (avatar + name + subtitle + chevron) ──────── */
422 .profile-trigger {
423 display: flex;
424 align-items: center;
425 gap: 12px;
426 padding: 4px 10px 4px 4px;
427 border-radius: 14px;
428 cursor: pointer;
429 user-select: none;
430 transition: background 0.18s, box-shadow 0.18s;
431 }
432 .profile-trigger:hover {
433 background: rgba(255,122,61,0.10);
434 box-shadow: inset 0 0 0 1px rgba(255,122,61,0.25);
435 }
436 .profile-menu:focus-within .profile-trigger {
437 background: rgba(255,122,61,0.14);
438 box-shadow: inset 0 0 0 1px rgba(255,122,61,0.45);
439 }
440 .profile-trigger-meta {
441 display: flex; flex-direction: column;
442 min-width: 0;
443 line-height: 1.15;
444 max-width: 200px;
445 }
446 .profile-trigger-name {
447 font-size: 13px;
448 font-weight: 700;
449 color: #fff;
450 white-space: nowrap;
451 overflow: hidden;
452 text-overflow: ellipsis;
453 }
454 .profile-trigger-role {
455 font-weight: 500;
456 color: var(--col-accent-bright);
457 opacity: 0.85;
458 }
459 .profile-trigger-sub {
460 margin-top: 2px;
461 font-size: 11px;
462 font-weight: 500;
463 letter-spacing: 0.3px;
464 color: var(--col-text-3);
465 white-space: nowrap;
466 overflow: hidden;
467 text-overflow: ellipsis;
468 }
469 .profile-chevron {
470 width: 14px; height: 14px;
471 color: var(--col-text-3);
472 flex-shrink: 0;
473 transition: transform 0.18s ease, color 0.18s;
474 }
475 .profile-menu:hover .profile-chevron,
476 .profile-menu:focus-within .profile-chevron {
477 transform: rotate(180deg);
478 color: var(--col-accent-bright);
479 }
480 @media (max-width: 720px) {
481 .profile-trigger-meta, .profile-chevron { display: none; }
482 .profile-trigger { padding: 0; gap: 0; }
483 .profile-trigger:hover, .profile-menu:focus-within .profile-trigger {
484 background: transparent; box-shadow: none;
485 }
486 }
487
488 /* ── Outline-style badges in dropdown header ─────────────────── */
489 .profile-dropdown-badges {
490 display: flex; flex-wrap: wrap; gap: 6px;
491 margin-top: 8px;
492 }
493 .profile-badge {
494 display: inline-flex; align-items: center;
495 padding: 3px 9px;
496 border-radius: 999px;
497 font-size: 10px;
498 font-weight: 700;
499 letter-spacing: 0.4px;
500 line-height: 1.4;
501 white-space: nowrap;
502 }
503 .profile-badge-violet {
504 background: transparent;
505 color: var(--col-accent-bright);
506 border: 1px solid var(--col-accent);
507 }
508 .profile-badge-neutral {
509 background: transparent;
510 color: var(--col-text-2);
511 border: 1px solid var(--col-border-2);
512 }
513
514 /* Avatar — sunset gradient + animated halo (per brand-warmth recipe). */
515 .profile-menu .avatar {
516 position: relative;
517 width: 38px; height: 38px;
518 border-radius: 50%;
519 background: linear-gradient(135deg, var(--col-accent) 0%, var(--col-accent-bright) 45%, #b8541f 100%);
520 display: flex; align-items: center; justify-content: center;
521 font-weight: 800; font-size: 14px; color: #1a0e08;
522 cursor: pointer;
523 letter-spacing: 0.5px;
524 user-select: none;
525 box-shadow:
526 0 0 0 2px rgba(255,122,61,0.45),
527 0 0 14px rgba(255,179,71,0.55);
528 transition: transform 0.18s ease, box-shadow 0.18s ease;
529 }
530 .profile-menu .avatar::before {
531 content: '';
532 position: absolute;
533 inset: -6px;
534 border-radius: 50%;
535 background: radial-gradient(circle, rgba(255,179,71,0.45) 0%, rgba(255,122,61,0) 70%);
536 animation: avatar-pulse 2.6s ease-in-out infinite;
537 pointer-events: none;
538 z-index: -1;
539 }
540 @keyframes avatar-pulse {
541 0%, 100% { transform: scale(1); opacity: 0.55; }
542 50% { transform: scale(1.25); opacity: 0.95; }
543 }
544 .profile-menu .avatar:hover {
545 transform: scale(1.08);
546 box-shadow:
547 0 0 0 3px rgba(255,179,71,0.65),
548 0 0 22px rgba(255,122,61,0.75);
549 }
550 .profile-menu:focus-within .avatar {
551 transform: scale(1.05);
552 outline: 2px solid var(--col-accent);
553 outline-offset: 3px;
554 }
555 @media (prefers-reduced-motion: reduce) {
556 .profile-menu .avatar::before { animation: none; }
557 }
558
559 /* Dropdown panel — wider 300px, brand-tinted shadow, scroll-capped. */
560 .profile-dropdown {
561 position: absolute;
562 top: calc(100% + 10px);
563 right: 0;
564 min-width: 300px;
565 background: var(--col-surface-2);
566 border: 1px solid var(--col-border-2);
567 border-radius: 14px;
568 padding: 8px 0 6px;
569 box-shadow: 0 24px 60px rgba(0,0,0,0.55), 0 0 24px rgba(255,122,61,0.12);
570 opacity: 0; visibility: hidden;
571 transform: translateY(-6px);
572 transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
573 z-index: 200;
574 max-height: calc(100vh - 90px);
575 overflow-y: auto;
576 overscroll-behavior: contain;
577 }
578 .profile-dropdown::-webkit-scrollbar { width: 8px; }
579 .profile-dropdown::-webkit-scrollbar-track { background: transparent; }
580 .profile-dropdown::-webkit-scrollbar-thumb {
581 background: rgba(148,163,184,0.25); border-radius: 4px;
582 }
583 .profile-dropdown::-webkit-scrollbar-thumb:hover {
584 background: rgba(148,163,184,0.45);
585 }
586 .profile-dropdown::before {
587 content: ""; position: absolute;
588 top: -8px; left: 0; right: 0; height: 8px;
589 }
590 .profile-menu:hover .profile-dropdown,
591 .profile-menu:focus-within .profile-dropdown {
592 opacity: 1; visibility: visible; transform: translateY(0);
593 }
594 /* Header: stacked name + email + badges. NO duplicate avatar — the
595 trigger already shows it. */
596 .profile-dropdown-header {
597 display: block;
598 padding: 14px 18px 12px;
599 }
600 .profile-dropdown-avatar { display: none; }
601 .profile-dropdown-name {
602 margin: 0 0 2px;
603 font-size: 15px; font-weight: 700;
604 color: #fff;
605 line-height: 1.3;
606 }
607 .profile-dropdown-email {
608 margin: 0;
609 font-size: 13px;
610 color: var(--col-text-3);
611 line-height: 1.35;
612 word-break: break-all;
613 }
614 .profile-dropdown-divider {
615 height: 1px;
616 background: var(--col-border);
617 margin: 4px 14px;
618 }
619 /* Section headers — small-caps with horizontal divider above each. */
620 .profile-dropdown-section {
621 font-size: 10px;
622 font-weight: 700;
623 letter-spacing: 1.8px;
624 text-transform: uppercase;
625 color: var(--col-text-3);
626 padding: 14px 18px 6px;
627 margin-top: 2px;
628 border-top: 1px solid var(--col-border);
629 }
630 .profile-dropdown-header + .profile-dropdown-section { border-top: 1px solid var(--col-border); margin-top: 6px; }
631 .profile-dropdown a {
632 display: flex; align-items: center;
633 padding: 9px 18px;
634 font-size: 14px; font-weight: 500;
635 color: var(--col-text);
636 text-decoration: none;
637 transition: color 0.12s, background 0.12s, padding 0.12s;
638 }
639 .profile-dropdown a:hover {
640 color: var(--col-accent-bright);
641 background: rgba(255,122,61,0.10);
642 padding-left: 22px;
643 }
644 .profile-dropdown a.profile-admin { color: var(--col-accent-bright); }
645 .profile-dropdown a.profile-signout { color: #f87171; }
646 .profile-dropdown a.profile-signout:hover { color: #fca5a5; background: rgba(239,68,68,0.08); }
647 /* TaskForge-style dropdown has no icons — hide auto-generated
648 icon spans/glyphs. */
649 .profile-dropdown .glyph,
650 .profile-dropdown .dropdown-icon { display: none; }
651 </style>
652 $extra_css
653 <script src="/assets/js/fingerprint.js" defer></script>
654</head>
655<body>
656 <div class="app">
657 <aside class="sidebar">
658 <div class="sidebar-brand">
659 <a href="/dashboard.cgi" style="display:flex;align-items:center;gap:12px;text-decoration:none">
660 <div class="brand-mark" style="padding:0">AB</div>
661 <div class="brand-text">
662 <span class="brand-name">$brand_name</span>
663 <span class="brand-sub">A/B MVT System</span>
664 </div>
665 </a>
666 </div>
667 <nav class="sidebar-nav">
668 $sidebar
669 </nav>
670 <div class="sidebar-foot">
671 <div class="user-chip">
672 <div class="user-avatar">$initials</div>
673 <div class="user-meta">
674 <div class="user-name">$display_name</div>
675 <div class="user-tier">$plan_tier Plan</div>
676 </div>
677 </div>
678 <a href="/logout.cgi" class="icon-btn" title="Sign out" aria-label="Sign out">
679 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
680 </a>
681 </div>
682 </aside>
683
684 <div class="app-main">
685 $impersonation_banner
686 <header class="topbar">
687 <!-- Hamburger toggle. Lives in the topbar (ABForge pattern) —
688 always discoverable, brand-orange chrome. Toggles
689 html.sidebar-collapsed. -->
690 <button class="sidebar-toggle" type="button" aria-label="Toggle sidebar" title="Collapse / expand sidebar">
691 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
692 </button>
693 <!-- Topbar mini brand, shown only when sidebar collapsed. -->
694 <a href="/dashboard.cgi" class="topbar-brand" aria-label="ABForge">
695 <div class="brand-mark" style="padding:0;width:34px;height:34px;font-size:15px">AB</div>
696 <span class="topbar-brand-name">$brand_name</span>
697 </a>
698 <div class="topbar-title">$title</div>
699 <div class="topbar-tools">
700 <form class="search-box" method="GET" action="/search.cgi" role="search">
701 <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
702 <input type="text" name="q" placeholder="Search experiments, sites, settings..." autocomplete="off">
703 </form>
704 [if:$is_admin]
705 <a id="chat-waiting-pill" class="chat-pill[if:$has_admin_chats_pending] cw-active[/if][if:!$has_admin_chats_pending] cw-idle[/if]" href="$admin_chats_href" title="Live chat">
706 <span class="cw-ping" aria-hidden="true"></span>
707 <span>Chat</span>
708 <span class="cw-badge[if:!$has_admin_chats_pending] cw-badge-empty[/if]"><span class="cw-count">$admin_chats_pending</span></span>
709 </a>
710 [/if]
711<div class="help-menu" tabindex="0">
712 <a class="help-btn" href="/help.cgi" title="Open the help &amp; resources guide" aria-label="Help" aria-haspopup="menu">
713 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><path d="M12 17h0"/></svg>
714 <span>Help</span>
715 <svg class="help-btn-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
716 </a>
717 <div class="help-dropdown" role="menu">
718 <div class="help-dropdown-section">Help</div>
719 <a href="/help.cgi" role="menuitem">
720 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><path d="M12 17h0"/></svg>
721 Help center
722 </a>
723 <a href="/tutorials.cgi" role="menuitem">
724 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>
725 Tutorials
726 </a>
727 <a href="/tutorials.cgi#section-optimization" role="menuitem">
728 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
729 A/B &amp; MVT tutorials
730 </a>
731 <a href="/tour.cgi" role="menuitem">
732 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
733 Product tour
734 </a>
735 </div>
736 </div>
737
738 <!-- Profile menu. Inline name+role+subtitle+chevron trigger
739 (TaskForge pattern). Dropdown auto-generated from
740 $dropdown_links — never hand-edit, keep in lockstep
741 with the left sidebar. -->
742 <div class="profile-menu" tabindex="0">
743 <div class="profile-trigger" title="My account">
744 <div class="avatar">$initials</div>
745 <div class="profile-trigger-meta">
746 <div class="profile-trigger-name">$display_name[if:$is_admin] <span class="profile-trigger-role">(Super Admin)</span>[/if]</div>
747 <div class="profile-trigger-sub">$plan_tier Plan</div>
748 </div>
749 <svg class="profile-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
750 </div>
751 <div class="profile-dropdown" role="menu">
752 <div class="profile-dropdown-header">
753 <div class="profile-dropdown-avatar">$initials</div>
754 <div style="min-width:0">
755 <p class="profile-dropdown-name">$display_name</p>
756 <p class="profile-dropdown-email">$email</p>
757 <div class="profile-dropdown-badges">
758 [if:$is_admin]<span class="profile-badge profile-badge-violet">Super Admin</span>[/if]
759 <span class="profile-badge profile-badge-neutral">$plan_tier</span>
760 </div>
761 </div>
762 </div>
763 $dropdown_links
764 <div class="profile-dropdown-divider"></div>
765 <a href="/logout.cgi" class="profile-signout"><span class="glyph">&times;</span> Sign Out</a>
766 </div>
767 </div>
768 </div>
769 </header>
770
771 <main class="app-content">
772 $body
773 </main>
774 </div>
775 </div>
776
777 <script src="$assets_js/site.js?v=2026-06-12-fixbrand"></script>
778 <script src="$assets_js/confirm.js?v=1"></script>
779
780 <script>
781 // Sidebar collapse toggle. Persists via /save_sidebar.cgi —
782 // Wrapper.pm renders the current value as a class on <html>, so
783 // the next page load is already in the right state on first paint.
784 (function () {
785 var btn = document.querySelector('.sidebar-toggle');
786 if (!btn) return;
787 var html = document.documentElement;
788 var mqMobile = window.matchMedia('(max-width: 1200px)');
789 // Tiny helper: persist a single sidebar field. Server endpoint
790 // updates only the field present in the POST so a desktop
791 // toggle never clobbers the mobile state and vice versa.
792 function saveSidebar(field, val) {
793 try {
794 fetch('/save_sidebar.cgi', {
795 method: 'POST',
796 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
797 body: field + '=' + (val ? '1' : '0'),
798 keepalive: true,
799 credentials: 'same-origin'
800 });
801 } catch (e2) {}
802 }
803
804 btn.addEventListener('click', function (e) {
805 if (mqMobile.matches) {
806 // On mobile the toggle controls the slide-in overlay.
807 // Stored under sidebar_mobile_open so the next page load
808 // boots in the same drawer state on the same phone.
809 var open = html.classList.toggle('mobile-nav-open');
810 saveSidebar('sidebar_mobile_open', open);
811 e.stopPropagation();
812 return;
813 }
814 var collapsed = html.classList.toggle('sidebar-collapsed');
815 saveSidebar('sidebar_collapsed', collapsed);
816 });
817
818
819 document.addEventListener('click', function (e) {
820 if (!html.classList.contains('mobile-nav-open')) return;
821 if (e.target.closest('.sidebar-toggle')) return;
822 if (e.target.closest('.sidebar a')) {
823 html.classList.remove('mobile-nav-open');
824 saveSidebar('sidebar_mobile_open', 0);
825 return;
826 }
827 if (!e.target.closest('.sidebar')) {
828 html.classList.remove('mobile-nav-open');
829 saveSidebar('sidebar_mobile_open', 0);
830 }
831 });
832
833 mqMobile.addEventListener('change', function (ev) {
834 if (!ev.matches) html.classList.remove('mobile-nav-open');
835 });
836 })();
837
838 // Live-poll the topbar chat pill (admin only).
839 (function () {
840 var pill = document.getElementById('chat-waiting-pill');
841 if (!pill) return;
842 var countEl = pill.querySelector('.cw-count');
843 var badgeEl = pill.querySelector('.cw-badge');
844 var POLL_MS = 20000;
845 var lastN = parseInt(countEl && countEl.textContent, 10) || 0;
846 var bumpTimer;
847 function paint(n, firstId) {
848 n = (typeof n === 'number' && n >= 0) ? n : 0;
849 firstId = (typeof firstId === 'number' && firstId > 0) ? firstId : 0;
850 if (n <= 0) {
851 pill.classList.remove('cw-active');
852 pill.classList.remove('cw-bump');
853 pill.classList.add('cw-idle');
854 pill.setAttribute('href', '/admin_chat.cgi');
855 pill.setAttribute('title', 'Live chat');
856 if (badgeEl) badgeEl.classList.add('cw-badge-empty');
857 lastN = 0;
858 return;
859 }
860 if (n !== lastN && countEl) countEl.textContent = n;
861 if (badgeEl) badgeEl.classList.remove('cw-badge-empty');
862 pill.classList.remove('cw-idle');
863 pill.classList.add('cw-active');
864 pill.setAttribute(
865 'href',
866 firstId ? ('/admin_chat.cgi?chat=' + firstId) : '/admin_chat.cgi?view=active'
867 );
868 pill.setAttribute('title', n + (n === 1 ? ' visitor' : ' visitors') + ' waiting for a reply');
869 if (n > lastN) {
870 pill.classList.remove('cw-bump');
871 void pill.offsetWidth;
872 pill.classList.add('cw-bump');
873 clearTimeout(bumpTimer);
874 bumpTimer = setTimeout(function () {
875 pill.classList.remove('cw-bump');
876 }, 500);
877 }
878 lastN = n;
879 }
880 function tick() {
881 fetch('/admin_chat_pending.cgi', {
882 credentials: 'same-origin',
883 cache: 'no-store'
884 })
885 .then(function (r) { return r.ok ? r.json() : null; })
886 .then(function (d) {
887 if (d && typeof d.n === 'number') paint(d.n, d.first_id || 0);
888 })
889 .catch(function () {});
890 }
891 setInterval(tick, POLL_MS);
892 window.addEventListener('focus', tick);
893 setInterval(function () {
894 if (pill.classList.contains('cw-active')) {
895 pill.classList.toggle('cw-blink-off');
896 } else {
897 pill.classList.remove('cw-blink-off');
898 }
899 }, 900);
900 })();
901
902// Preserve sidebar scroll position across page reloads.
903 (function () {
904 var nav = document.querySelector('.sidebar-nav');
905 if (!nav) return;
906 var KEY = 'abforge_sidebar_scroll';
907 var saved = sessionStorage.getItem(KEY);
908 if (saved !== null) {
909 var n = parseInt(saved, 10);
910 if (!isNaN(n) && n >= 0) nav.scrollTop = n;
911 }
912 var pending;
913 nav.addEventListener('scroll', function () {
914 clearTimeout(pending);
915 pending = setTimeout(function () {
916 try { sessionStorage.setItem(KEY, nav.scrollTop); } catch (e) {}
917 }, 120);
918 }, { passive: true });
919 window.addEventListener('beforeunload', function () {
920 try { sessionStorage.setItem(KEY, nav.scrollTop); } catch (e) {}
921 });
922 })();
923 </script>
924 $extra_js
925 <script>
926 // Timezone pingback.
927 (function(){
928 try {
929 var tz = (Intl && Intl.DateTimeFormat) ? Intl.DateTimeFormat().resolvedOptions().timeZone : '';
930 if (!tz) return;
931 var sent = '';
932 try { sent = sessionStorage.getItem('abforge_tz_sent'); } catch(e){}
933 if (sent === tz) return;
934 var fd = new FormData();
935 fd.append('tz', tz);
936 fetch('/_set_timezone.cgi', { method: 'POST', body: fd, credentials: 'same-origin' })
937 .then(function(){ try { sessionStorage.setItem('abforge_tz_sent', tz); } catch(e){} })
938 .catch(function(){});
939 } catch(e) {}
940 })();
941 </script>
942<!-- Viewer-local timestamp renderer (portfolio-canonical).
943 Server emits `<span class="ts" data-ts="EPOCH" data-fmt="…">FALLBACK</span>`.
944 Formats: datetime | date | date-short | time | relative
945 Original text is the no-JS fallback. Re-run via window.tzLocalize() after AJAX. -->
946<script>
947(function(){
948 function fmt(el){
949 var e = parseInt(el.getAttribute('data-ts'), 10);
950 if (!e) return;
951 var d = new Date(e * 1000);
952 var kind = el.getAttribute('data-fmt') || 'datetime';
953 var out;
954 switch (kind) {
955 case 'date': out = d.toLocaleDateString(undefined, {year:'numeric',month:'short',day:'numeric'}); break;
956 case 'date-short': out = d.toLocaleDateString(undefined, {month:'short',day:'numeric'}); break;
957 case 'time': out = d.toLocaleTimeString(undefined, {hour:'2-digit',minute:'2-digit'}); break;
958 case 'relative': out = rel(e); break;
959 default: out = d.toLocaleDateString(undefined, {year:'numeric',month:'short',day:'numeric'})
960 + ' ' + d.toLocaleTimeString(undefined, {hour:'2-digit',minute:'2-digit'});
961 }
962 el.textContent = out;
963 if (!el.title) el.title = d.toString();
964 }
965 function rel(e){
966 var s = Math.floor(Date.now()/1000) - e, future = s < 0; s = Math.abs(s);
967 var n, u;
968 if (s < 60) { n = s; u = 'second'; }
969 else if (s < 3600) { n = Math.floor(s/60); u = 'minute'; }
970 else if (s < 86400) { n = Math.floor(s/3600); u = 'hour'; }
971 else if (s < 2592000){ n = Math.floor(s/86400); u = 'day'; }
972 else if (s < 31536000){n = Math.floor(s/2592000); u = 'month'; }
973 else { n = Math.floor(s/31536000);u = 'year'; }
974 var p = n === 1 ? '' : 's';
975 return future ? ('in ' + n + ' ' + u + p) : (n + ' ' + u + p + ' ago');
976 }
977 function localize(root){ (root||document).querySelectorAll('span.ts[data-ts], time[data-ts]').forEach(fmt); }
978 window.tzLocalize = localize;
979 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){ localize(); });
980 else localize();
981 window.addEventListener('ts-refresh', function(){ localize(); });
982})();
983</script>
984
985</body>
986</html>