PageSourceSearch

https://ottumwapost.com/sys-admin-appearance.js

js ottumwapost.com collected 2026-09-26 13:25:58 UTC 62,775 bytes, 1,358 lines download raw bytes

1// sys-admin-appearance.js - Admin Click-to-Style Visual Editor
2// Only loads for logged-in admins (detected via _cms_admin hint cookie)
3(function() {
4  'use strict';
5  var _bp = (typeof __CMS_BASE_PATH !== 'undefined' ? __CMS_BASE_PATH : '');
6
7  // Gate: only run for admins
8  if (document.cookie.indexOf('_cms_admin=1') === -1) return;
9
10  var PREFIX = 'sys-ap';
11
12  var DEFAULTS = {
13    text: '#1f2937', heading: '#111827', link: '#2563eb',
14    background: '#ffffff', navBg: '#1a1a1a', navText: '#ffffff',
15    dropdownBg: '#ffffff', dropdownText: '#1f2937',
16    footerBg: '#1a1a1a', footerText: '#ffffff', accent: '#2563eb', border: '#e5e7eb'
17  };
18
19  var FM = {
20    system: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif",
21    arial: 'Arial,Helvetica,sans-serif',
22    georgia: 'Georgia,serif',
23    times: "'Times New Roman',Times,serif",
24    verdana: 'Verdana,Geneva,sans-serif',
25    tahoma: 'Tahoma,Geneva,sans-serif',
26    trebuchet: "'Trebuchet MS',Helvetica,sans-serif",
27    roboto: 'Roboto,sans-serif',
28    opensans: "'Open Sans',sans-serif",
29    merriweather: 'Merriweather,serif',
30    lato: 'Lato,sans-serif',
31    montserrat: 'Montserrat,sans-serif',
32    poppins: 'Poppins,sans-serif',
33    raleway: 'Raleway,sans-serif',
34    nunito: 'Nunito,sans-serif',
35    playfair: "'Playfair Display',serif"
36  };
37
38  var GF = {
39    roboto: 'Roboto:wght@400;700', opensans: 'Open+Sans:wght@400;700',
40    merriweather: 'Merriweather:wght@400;700', lato: 'Lato:wght@400;700',
41    montserrat: 'Montserrat:wght@400;700', poppins: 'Poppins:wght@400;700',
42    raleway: 'Raleway:wght@400;700', nunito: 'Nunito:wght@400;700',
43    playfair: 'Playfair+Display:wght@400;700'
44  };
45
46  var THEME_FILTERS = {
47    light: '', dark: 'invert(1) hue-rotate(180deg)',
48    sepia: 'sepia(0.5) saturate(1.2) brightness(0.92)',
49    highContrast: 'invert(1) hue-rotate(180deg) contrast(1.3)'
50  };
51  var INVERT_THEMES = { dark: true, highContrast: true };
52
53  var BASE_FONTS = { base: 14, sm: 12, lg: 16, xl: 18, xxl: 24 };
54  var BASE_SPACING = { xs: 4, sm: 8, md: 16, lg: 24, xl: 32 };
55
56  // Load current settings
57  var appearance = window.__CMS_APPEARANCE || {};
58  var savedColors = appearance.colors || {};
59  var state = {
60    theme: appearance.theme || '',
61    fontScale: appearance.fontScale || 1,
62    spacingScale: appearance.spacingScale || 1,
63    fontFamily: appearance.fontFamily || 'system',
64    colors: {},
65    menuStyles: appearance.menuStyles || {},
66    categoryStyles: appearance.categoryStyles || {}
67  };
68  var dk = Object.keys(DEFAULTS);
69  dk.forEach(function(k) { if (savedColors[k]) state.colors[k] = savedColors[k]; });
70
71  // Style controls available per category key
72  var STYLE_CONTROLS = {
73    heading:     ['fontWeight', 'fontSize', 'textTransform'],
74    text:        ['fontWeight', 'fontSize', 'textTransform'],
75    link:        ['fontWeight', 'fontSize', 'textTransform'],
76    accent:      ['borderRadius', 'textTransform'],
77    border:      ['textTransform'],
78    menuItem:    ['fontWeight', 'fontSize', 'textTransform'],
79    submenuItem: ['fontWeight', 'textTransform'],
80    navBg:       ['textTransform'],
81    dropdownBg:  ['textTransform'],
82    footerBg:    ['textTransform']
83  };
84
85  var STYLE_LABELS = {
86    fontWeight: 'Weight',
87    fontSize: 'Size',
88    textTransform: 'Case',
89    borderRadius: 'Corners'
90  };
91
92  // CSS selectors per category for applying styles
93  var CATEGORY_CSS = {
94    heading: ':root h1,:root h2,:root h3,:root h4,:root h5,:root h6,:root .pageheading,:root .layout_pageheading',
95    text: ':root p,:root li,:root td,:root dd,:root span,:root .pagebody,:root .layout_pagebody',
96    link: ':root a:not(.nav-link):not(.navbar-brand):not(.dropdown-item)',
97    navBg: ':root #qm0>a,:root #qm1>a,:root .qmmc>a,:root .nav-link,:root .navbar-brand',
98    footerBg: ':root .cus-submenu-footer,:root .cus-submenu-footer a,:root .cus-submenu-footer p,:root .cus-submenu-footer span',
99    accent: ':root .btn-primary,:root .btn-primary:hover,:root .read-more,:root .badge-primary,:root .cus-scroll-top'
100  };
101
102  // === CATEGORY MAP ===
103
104  var CATEGORY_MAP = [
105    { key: 'menuItem', label: 'Menu Item', color: '#7c3aed',
106      controls: [], // dynamic - handled by detectMenuItem
107      match: function(el) { return !!(el.closest('[data-cms-menu-id]') || el.closest('.menubodyhorizontal') || el.closest('.menubodydefault')); } },
108    { key: 'submenuItem', label: 'Submenu Item', color: '#a78bfa',
109      controls: [],
110      match: function(el) { return !!(el.closest('[data-cms-submenu-id]') || el.closest('.submenubodyhorizontal') || el.closest('.submenubodydefault')); } },
111    { key: 'navBg', label: 'Navbar', color: '#8b5cf6',
112      controls: ['navBg', 'navText'],
113      match: function(el) { return !!(el.closest('#qm0,#qm1,.qmmc,.navbar,.cus-submenu-header,.column6')); } },
114    { key: 'dropdownBg', label: 'Dropdown', color: '#6366f1',
115      controls: ['dropdownBg', 'dropdownText'],
116      match: function(el) { return !!(el.closest('.dropdown-menu,.SubMenuContainer1')); } },
117    { key: 'footerBg', label: 'Footer', color: '#f97316',
118      controls: ['footerBg', 'footerText'],
119      match: function(el) { return !!(el.closest('.cus-submenu-footer')); } },
120    { key: 'heading', label: 'Headings', color: '#ec4899',
121      controls: ['heading', 'background'],
122      match: function(el) { return /^H[1-6]$/.test(el.tagName) || el.classList.contains('pageheading') || el.classList.contains('layout_pageheading'); } },
123    { key: 'link', label: 'Links', color: '#3b82f6',
124      controls: ['link', 'background'],
125      match: function(el) { return el.tagName === 'A' && !el.classList.contains('nav-link') && !el.classList.contains('navbar-brand') && !el.classList.contains('dropdown-item'); } },
126    { key: 'accent', label: 'Accent / Buttons', color: '#10b981',
127      controls: ['accent', 'background'],
128      match: function(el) { return el.classList.contains('btn-primary') || el.classList.contains('read-more') || el.classList.contains('badge-primary') || el.classList.contains('cus-scroll-top'); } },
129    { key: 'border', label: 'Borders', color: '#6b7280',
130      controls: ['border'],
131      match: function(el) { return el.tagName === 'HR' || el.classList.contains('card') || el.classList.contains('border') || el.tagName === 'TABLE'; } },
132    { key: 'background', label: 'Page Background', color: '#f59e0b',
133      controls: ['background'],
134      match: function(el) { return el.tagName === 'BODY' || el.classList.contains('pagebody') || el.classList.contains('layout_pagebody'); } },
135    { key: 'text', label: 'Body Text', color: '#64748b',
136      controls: ['text', 'background'],
137      match: function(el) { return /^(P|LI|TD|DD|SPAN|DIV)$/.test(el.tagName); } }
138  ];
139
140  var COLOR_LABELS = {
141    text: 'Body Text', heading: 'Headings', link: 'Links',
142    background: 'Background', accent: 'Accent', border: 'Borders',
143    navBg: 'Navbar BG', navText: 'Navbar Text',
144    dropdownBg: 'Dropdown BG', dropdownText: 'Dropdown Text',
145    footerBg: 'Footer BG', footerText: 'Footer Text'
146  };
147
148  function detectCategory(el) {
149    // Skip non-stylable elements (images, video, iframes, canvas, svg)
150    if (/^(IMG|VIDEO|IFRAME|CANVAS|SVG|PICTURE|SOURCE)$/.test(el.tagName)) return null;
151    for (var i = 0; i < CATEGORY_MAP.length; i++) {
152      if (CATEGORY_MAP[i].match(el)) return CATEGORY_MAP[i];
153    }
154    if (el.parentElement && el.parentElement !== document.documentElement && el.parentElement !== document.body) {
155      return detectCategory(el.parentElement);
156    }
157    return null;
158  }
159
160  function detectMenuItem(el) {
161    // Check for individual menu item
162    var menuA = el.closest('[data-cms-menu-id]');
163    var menuDiv = el.closest('.menubodyhorizontal') || el.closest('.menubodydefault');
164    var menuEl = menuA || menuDiv;
165    if (menuEl) {
166      var id = null;
167      if (menuA) id = menuA.getAttribute('data-cms-menu-id');
168      if (!id && menuDiv) {
169        var cls = menuDiv.className || '';
170        var m = cls.match(/custom_menu_(\d+)/);
171        if (m) id = m[1];
172      }
173      if (!id && menuDiv) {
174        // Try data attribute on child anchor
175        var innerA = menuDiv.querySelector('[data-cms-menu-id]');
176        if (innerA) id = innerA.getAttribute('data-cms-menu-id');
177      }
178      var label = (menuEl.textContent || '').trim().replace(/[\n\r]+/g, ' ').substring(0, 25);
179      return { type: 'menu', id: id, label: label, el: menuEl };
180    }
181    // Check for submenu item
182    var subA = el.closest('[data-cms-submenu-id]');
183    var subDiv = el.closest('.submenubodyhorizontal') || el.closest('.submenubodydefault');
184    var subEl = subA || subDiv;
185    if (subEl) {
186      var subId = null;
187      if (subA) subId = subA.getAttribute('data-cms-submenu-id');
188      if (!subId && subDiv) {
189        var scls = subDiv.className || '';
190        var sm = scls.match(/custom_Submenu_(\d+)/);
191        if (sm) subId = sm[1];
192      }
193      if (!subId && subDiv) {
194        var innerSub = subDiv.querySelector('[data-cms-submenu-id]');
195        if (innerSub) subId = innerSub.getAttribute('data-cms-submenu-id');
196      }
197      var subLabel = (subEl.textContent || '').trim().replace(/[\n\r]+/g, ' ').substring(0, 25);
198      return { type: 'submenu', id: subId, label: subLabel, el: subEl };
199    }
200    return null;
201  }
202
203  // === LIVE APPLICATION (unchanged logic) ===
204
205  function resolveTheme(t) {
206    if (t === 'auto' || !t) return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
207    return t;
208  }
209
210  function applyTheme(themeName) {
211    var resolved = resolveTheme(themeName);
212    document.documentElement.setAttribute('data-theme', resolved);
213    var el = document.getElementById(PREFIX + '-overrides');
214    if (resolved === 'light') {
215      if (el) el.textContent = '';
216      document.body.style.filter = '';
217      document.body.style.background = '';
218      applyColors();
219      return;
220    }
221    var colorEl = document.getElementById(PREFIX + '-colors');
222    if (colorEl) colorEl.textContent = '';
223    var upColors = document.getElementById('sys-up-admin-colors');
224    if (upColors) upColors.textContent = '';
225
226    var filterVal = THEME_FILTERS[resolved] || '';
227    if (!filterVal) return;
228    document.body.style.filter = filterVal;
229    document.body.style.background = '#fff';
230
231    var css = '';
232    if (INVERT_THEMES[resolved]) {
233      css = 'body[style*="filter"] img,body[style*="filter"] video,body[style*="filter"] iframe,' +
234        'body[style*="filter"] canvas,body[style*="filter"] svg image,' +
235        'body[style*="filter"] [style*="background-image"]{filter:' + filterVal + ' !important}';
236    }
237    if (!el) { el = document.createElement('style'); el.id = PREFIX + '-overrides'; document.head.appendChild(el); }
238    el.textContent = css;
239  }
240
241  function applyFontScale(scale) {
242    var r = document.documentElement.style;
243    r.setProperty('--font-size-base', (BASE_FONTS.base * scale) + 'px');
244    r.setProperty('--font-size-sm', (BASE_FONTS.sm * scale) + 'px');
245    r.setProperty('--font-size-lg', (BASE_FONTS.lg * scale) + 'px');
246    r.setProperty('--font-size-xl', (BASE_FONTS.xl * scale) + 'px');
247    r.setProperty('--font-size-xxl', (BASE_FONTS.xxl * scale) + 'px');
248    document.documentElement.style.fontSize = (100 * scale) + '%';
249  }
250
251  function applySpacingScale(scale) {
252    var r = document.documentElement.style;
253    r.setProperty('--spacing-xs', (BASE_SPACING.xs * scale) + 'px');
254    r.setProperty('--spacing-sm', (BASE_SPACING.sm * scale) + 'px');
255    r.setProperty('--spacing-md', (BASE_SPACING.md * scale) + 'px');
256    r.setProperty('--spacing-lg', (BASE_SPACING.lg * scale) + 'px');
257    r.setProperty('--spacing-xl', (BASE_SPACING.xl * scale) + 'px');
258  }
259
260  function applyFontFamily(key) {
261    var css = FM[key] || FM.system;
262    var linkEl = document.getElementById(PREFIX + '-gfont');
263    if (GF[key]) {
264      var href = 'https://fonts.googleapis.com/css2?family=' + GF[key] + '&display=swap';
265      if (!linkEl) { linkEl = document.createElement('link'); linkEl.id = PREFIX + '-gfont'; linkEl.rel = 'stylesheet'; document.head.appendChild(linkEl); }
266      linkEl.href = href;
267    } else {
268      if (linkEl) linkEl.parentNode.removeChild(linkEl);
269    }
270    var fontEl = document.getElementById(PREFIX + '-font');
271    if (!fontEl) { fontEl = document.createElement('style'); fontEl.id = PREFIX + '-font'; document.head.appendChild(fontEl); }
272    if (key && key !== 'system') {
273      fontEl.textContent = 'body{font-family:' + css + ' !important}';
274    } else {
275      fontEl.textContent = '';
276    }
277  }
278
279  // Specificity boost: :root prefix adds pseudo-class specificity (0,1,0) to every selector.
280  // :root always matches <html>, needs zero JS. :root h2 (0,1,1) beats .text-white (0,1,0).
281  var SP = ':root';
282
283  function _buildColorCss(cc) {
284    var css = '';
285    if (cc.text) css += SP + ' body,' + SP + ' p,' + SP + ' li,' + SP + ' td,' + SP + ' dd,' + SP + ' span,' + SP + ' div,' + SP + ' .text-dark,' + SP + ' .text-black,' + SP + ' .text-muted,' + SP + ' .text-white,' + SP + ' .pagebody,' + SP + ' .layout_pagebody,' + SP + ' .pagebody p,' + SP + ' .pagebody li,' + SP + ' .pagebody td,' + SP + ' .pagebody span{color:' + cc.text + ' !important}';
286    if (cc.heading) css += SP + ' h1,' + SP + ' h2,' + SP + ' h3,' + SP + ' h4,' + SP + ' h5,' + SP + ' h6,' + SP + ' .pageheading,' + SP + ' .layout_pageheading{color:' + cc.heading + ' !important}';
287    if (cc.link) css += SP + ' a:not(.nav-link):not(.navbar-brand):not(.dropdown-item){color:' + cc.link + ' !important}';
288    if (cc.background && cc.background !== '#ffffff') css += SP + ' body,' + SP + ' .pagebody,' + SP + ' .layout_pagebody{background-color:' + cc.background + ' !important}';
289    if (cc.navBg) css += SP + ' #qm0,' + SP + ' #qm1,' + SP + ' .qmmc,' + SP + ' .navbar,' + SP + ' .cus-submenu-header{background-color:' + cc.navBg + ' !important}';
290    if (cc.navText) css += SP + ' #qm0>a,' + SP + ' #qm1>a,' + SP + ' .qmmc>a,' + SP + ' .nav-link,' + SP + ' .navbar-brand{color:' + cc.navText + ' !important}';
291    var ddBg = cc.dropdownBg || cc.navBg;
292    var ddText = cc.dropdownText || cc.navText;
293    if (ddBg) css += SP + ' #qm0 div,' + SP + ' #qm1 div,' + SP + ' .qmmc div,' + SP + ' #qm0 div a,' + SP + ' #qm1 div a,' + SP + ' .qmmc div a,' + SP + ' #qm0 div td,' + SP + ' #qm1 div td,' + SP + ' .qmmc div td,' + SP + ' .navbar .dropdown-menu,' + SP + ' .dropdown-menu li{background-color:' + ddBg + ' !important}';
294    if (ddText) css += SP + ' #qm0 div a,' + SP + ' #qm1 div a,' + SP + ' .qmmc div a,' + SP + ' .dropdown-item,' + SP + ' .dropdown-menu a,' + SP + ' .dropdown-menu li a{color:' + ddText + ' !important}';
295    if (cc.footerBg) css += SP + ' .cus-submenu-footer{background-color:' + cc.footerBg + ' !important}';
296    if (cc.footerText) css += SP + ' .cus-submenu-footer,' + SP + ' .cus-submenu-footer a,' + SP + ' .cus-submenu-footer p,' + SP + ' .cus-submenu-footer span,' + SP + ' .cus-submenu-footer li{color:' + cc.footerText + ' !important}';
297    if (cc.accent) css += SP + ' .btn-primary,' + SP + ' .btn-primary:hover{background-color:' + cc.accent + ' !important;border-color:' + cc.accent + ' !important}' + SP + ' .read-more,' + SP + ' .badge-primary,' + SP + ' .page-item.active .page-link{background-color:' + cc.accent + ' !important}' + SP + ' .cus-scroll-top{background-color:' + cc.accent + ' !important}';
298    if (cc.border) css += SP + ' hr,' + SP + ' .card,' + SP + ' .border{border-color:' + cc.border + ' !important}' + SP + ' table td,' + SP + ' table th{border-color:' + cc.border + ' !important}';
299    return css;
300  }
301
302  function applyColors() {
303    var el = document.getElementById(PREFIX + '-colors');
304    var css = _buildColorCss(state.colors);
305
306    if (!el) { el = document.createElement('style'); el.id = PREFIX + '-colors'; document.head.appendChild(el); }
307    el.textContent = css;
308    document.head.appendChild(el);
309
310    var upColors = document.getElementById('sys-up-admin-colors');
311    if (upColors) upColors.textContent = '';
312  }
313
314  function applyMenuStyles() {
315    var el = document.getElementById(PREFIX + '-menu-colors');
316    if (!el) { el = document.createElement('style'); el.id = PREFIX + '-menu-colors'; document.head.appendChild(el); }
317    var css = '';
318    var ms = state.menuStyles;
319    for (var key in ms) {
320      if (!ms.hasOwnProperty(key)) continue;
321      var parts = key.split('_');
322      var type = parts[0];
323      var id = parts.slice(1).join('_');
324      var styles = ms[key];
325      // Build extra style props (fontWeight, fontSize, textTransform)
326      var extra = '';
327      if (styles.fontWeight && styles.fontWeight !== 'normal') extra += 'font-weight:' + styles.fontWeight + ' !important;';
328      if (styles.fontSize) extra += 'font-size:' + styles.fontSize + 'em !important;';
329      if (styles.textTransform) extra += 'text-transform:' + styles.textTransform + ' !important;';
330
331      if (type === 'menu') {
332        var mSel = '[data-cms-menu-id="' + id + '"],.custom_menu_' + id + ',.custom_menu_' + id + '>a';
333        if (styles.bg) css += mSel + '{background-color:' + styles.bg + ' !important}';
334        if (styles.text) css += mSel + '{color:' + styles.text + ' !important}';
335        if (extra) css += mSel + '{' + extra + '}';
336      } else if (type === 'submenu') {
337        var sSel = '[data-cms-submenu-id="' + id + '"],.custom_Submenu_' + id + ',.custom_Submenu_' + id + ' a';
338        if (styles.bg) css += sSel + '{background-color:' + styles.bg + ' !important}';
339        if (styles.text) css += sSel + '{color:' + styles.text + ' !important}';
340        if (extra) css += sSel + '{' + extra + '}';
341      }
342    }
343    el.textContent = css;
344    document.head.appendChild(el);
345  }
346
347  function applyCategoryStyles() {
348    var el = document.getElementById(PREFIX + '-cat-styles');
349    if (!el) { el = document.createElement('style'); el.id = PREFIX + '-cat-styles'; document.head.appendChild(el); }
350    var css = '';
351    var cs = state.categoryStyles;
352    for (var catKey in cs) {
353      if (!cs.hasOwnProperty(catKey) || !CATEGORY_CSS[catKey]) continue;
354      var styles = cs[catKey];
355      var props = '';
356      if (styles.fontWeight && styles.fontWeight !== 'normal') props += 'font-weight:' + styles.fontWeight + ' !important;';
357      if (styles.fontSize) props += 'font-size:' + styles.fontSize + 'em !important;';
358      if (styles.textTransform) props += 'text-transform:' + styles.textTransform + ' !important;';
359      if (styles.borderRadius !== undefined && styles.borderRadius !== null && styles.borderRadius !== '') props += 'border-radius:' + styles.borderRadius + 'px !important;';
360      if (props) css += CATEGORY_CSS[catKey] + '{' + props + '}';
361    }
362    el.textContent = css;
363    document.head.appendChild(el);
364  }
365
366  function applyAll() {
367    applyTheme(state.theme);
368    applyFontScale(state.fontScale);
369    applySpacingScale(state.spacingScale);
370    applyFontFamily(state.fontFamily);
371    applyMenuStyles();
372    applyCategoryStyles();
373  }
374
375  // === UI STATE ===
376
377  var designMode = false;
378  var fabEl, topBarEl, highlightEl, popoverEl, toastEl;
379  var _highlightTarget = null;
380  var _popoverTarget = null;
381  var _popoverCategory = null;
382  var _popoverMenuItem = null;
383  var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
384  var transition = reducedMotion ? 'none' : 'all 0.25s ease';
385  var _rafId = null;
386
387  // === INJECT STYLES ===
388
389  function injectStyles() {
390    var s = document.createElement('style');
391    s.id = PREFIX + '-css';
392    s.textContent =
393      // FAB
394      '@keyframes ' + PREFIX + '-pulse{0%,100%{box-shadow:0 0 8px rgba(234,88,12,0.4)}50%{box-shadow:0 0 20px rgba(234,88,12,0.8),0 0 40px rgba(234,88,12,0.3)}}' +
395      '#' + PREFIX + '-fab{position:fixed;top:16px;right:16px;width:44px;height:44px;border-radius:50%;' +
396        'background:#ea580c;color:#fff;border:none;cursor:pointer;z-index:10000;' +
397        'display:flex;align-items:center;justify-content:center;font-size:18px;' +
398        'animation:' + PREFIX + '-pulse 2s ease-in-out infinite;transition:' + transition + ';outline:none}' +
399      '#' + PREFIX + '-fab:hover{transform:scale(1.1);background:#c2410c}' +
400      '#cms-lb-hdr #' + PREFIX + '-fab{position:static;width:auto;height:auto;border-radius:6px;' +
401        'background:transparent;font-size:13px;animation:none;padding:8px 14px;gap:6px;' +
402        'color:#e2e8f0 !important;font-family:inherit;white-space:nowrap}' +
403      '#cms-lb-hdr #' + PREFIX + '-fab:hover{transform:none;background:rgba(255,255,255,0.15);color:#fff !important}' +
404
405      // Top Bar
406      '#' + PREFIX + '-topbar{position:fixed;top:-60px;left:0;width:100%;height:56px;' +
407        'background:linear-gradient(135deg,#c2410c 0%,#ea580c 50%,#f97316 100%);color:#fff;z-index:99994;display:flex;align-items:center;' +
408        'justify-content:space-between;padding:0 12px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' +
409        'font-size:13px;box-shadow:0 2px 12px rgba(0,0,0,0.3);transition:top 0.3s ease}' +
410      '#' + PREFIX + '-topbar *{color:#fff !important;font-family:inherit !important}' +
411      '#' + PREFIX + '-topbar.open{top:0}' +
412      '.' + PREFIX + '-tb-left{display:flex;align-items:center;gap:8px;flex-shrink:0}' +
413      '.' + PREFIX + '-tb-left i{font-size:16px;color:#fff !important}' +
414      '.' + PREFIX + '-tb-label{font-weight:700;font-size:13px;white-space:nowrap}' +
415      '.' + PREFIX + '-tb-dot{width:8px;height:8px;border-radius:50%;background:#22c55e;animation:' + PREFIX + '-pulse-dot 1.5s ease-in-out infinite}' +
416      '@keyframes ' + PREFIX + '-pulse-dot{0%,100%{opacity:1}50%{opacity:0.4}}' +
417      '.' + PREFIX + '-tb-center{display:flex;align-items:center;gap:6px;flex:1;justify-content:center;overflow-x:auto}' +
418      '.' + PREFIX + '-tb-group{display:flex;align-items:center;gap:2px;background:rgba(255,255,255,0.15);border-radius:6px;padding:2px}' +
419      '.' + PREFIX + '-tb-group-label{font-size:9px;text-transform:uppercase;letter-spacing:0.05em;color:rgba(255,255,255,0.7) !important;padding:0 4px;white-space:nowrap}' +
420      '.' + PREFIX + '-tb-btn{background:none;border:none;color:rgba(255,255,255,0.85) !important;cursor:pointer;padding:5px 8px;border-radius:4px;font-size:12px;white-space:nowrap;outline:none}' +
421      '.' + PREFIX + '-tb-btn:hover{background:rgba(255,255,255,0.2)}' +
422      '.' + PREFIX + '-tb-btn.active{background:rgba(0,0,0,0.3) !important;color:#fff !important;font-weight:700}' +
423      '.' + PREFIX + '-tb-select{background:rgba(255,255,255,0.15);border:1px solid rgba(255,255,255,0.3);color:#fff !important;border-radius:4px;' +
424        'padding:4px 6px;font-size:11px;cursor:pointer;outline:none;max-width:120px}' +
425      '.' + PREFIX + '-tb-right{display:flex;align-items:center;gap:6px;flex-shrink:0}' +
426      '.' + PREFIX + '-tb-save{background:rgba(0,0,0,0.3) !important;color:#fff !important;border:1px solid rgba(255,255,255,0.4);border-radius:6px;padding:6px 14px;font-size:12px;font-weight:600;cursor:pointer}' +
427      '.' + PREFIX + '-tb-save:hover{background:rgba(0,0,0,0.45) !important;border-color:#fff}' +
428      '.' + PREFIX + '-tb-save:disabled{opacity:0.5;cursor:not-allowed}' +
429      '.' + PREFIX + '-tb-reset{background:transparent;border:1px solid rgba(255,255,255,0.4);color:#fff !important;border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer}' +
430      '.' + PREFIX + '-tb-reset:hover{background:rgba(255,255,255,0.15);border-color:#fff}' +
431      '.' + PREFIX + '-tb-exit{background:none;border:none;color:rgba(255,255,255,0.7) !important;cursor:pointer;font-size:20px;padding:4px 8px;line-height:1;border-radius:4px}' +
432      '.' + PREFIX + '-tb-exit:hover{background:rgba(255,255,255,0.15);color:#fff !important}' +
433
434      // Highlight
435      '#' + PREFIX + '-highlight{position:fixed;pointer-events:none;z-index:99990;border:2px solid transparent;' +
436        'border-radius:2px;background:rgba(37,99,235,0.04);transition:all 0.12s ease;display:none}' +
437      '#' + PREFIX + '-highlight.visible{display:block}' +
438      '.' + PREFIX + '-cat-badge{position:absolute;top:-22px;left:0;color:#fff !important;font-size:10px;padding:2px 8px;' +
439        'border-radius:3px;white-space:nowrap;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}' +
440
441      // Popover
442      '#' + PREFIX + '-popover{position:fixed;z-index:99995;width:280px;background:#fff;border-radius:10px;' +
443        'box-shadow:0 8px 30px rgba(0,0,0,0.18),0 2px 8px rgba(0,0,0,0.1);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' +
444        'font-size:13px;opacity:0;transform:translateY(6px);pointer-events:none;transition:opacity 0.2s,transform 0.2s}' +
445      '#' + PREFIX + '-popover *{color:#1f2937 !important;font-family:inherit !important}' +
446      '#' + PREFIX + '-popover.open{opacity:1;transform:translateY(0);pointer-events:auto}' +
447      '.' + PREFIX + '-pop-hdr{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid #e5e7eb}' +
448      '.' + PREFIX + '-pop-title{display:flex;align-items:center;gap:6px;font-weight:700;font-size:13px}' +
449      '.' + PREFIX + '-pop-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}' +
450      '.' + PREFIX + '-pop-close{background:none;border:none;font-size:18px;cursor:pointer;color:#9ca3af !important;padding:2px 6px;border-radius:4px;line-height:1}' +
451      '.' + PREFIX + '-pop-close:hover{background:#f3f4f6;color:#374151 !important}' +
452      '.' + PREFIX + '-pop-body{padding:10px 14px}' +
453      '.' + PREFIX + '-pop-row{display:flex;align-items:center;gap:8px;margin-bottom:8px}' +
454      '.' + PREFIX + '-pop-row:last-child{margin-bottom:0}' +
455      '.' + PREFIX + '-pop-row label{font-size:12px;font-weight:500;color:#374151 !important;white-space:nowrap;min-width:60px}' +
456      '.' + PREFIX + '-pop-row input[type="color"]{width:32px;height:26px;border:2px solid #e5e7eb;border-radius:5px;padding:1px;cursor:pointer;background:#fff}' +
457      '.' + PREFIX + '-pop-row .hex{font-size:11px;color:#6b7280 !important;font-family:monospace !important}' +
458      '.' + PREFIX + '-pop-opts{display:flex;gap:3px;flex:1}' +
459      '.' + PREFIX + '-pop-opt{padding:3px 8px;border:1px solid #d1d5db;border-radius:4px;background:#fff;cursor:pointer;font-size:11px;color:#374151 !important;white-space:nowrap}' +
460      '.' + PREFIX + '-pop-opt:hover{background:#f3f4f6;border-color:#9ca3af}' +
461      '.' + PREFIX + '-pop-opt.active{background:#ea580c !important;border-color:#ea580c;color:#fff !important;font-weight:600}' +
462      '.' + PREFIX + '-pop-sel{flex:1;padding:3px 6px;border:1px solid #d1d5db;border-radius:4px;font-size:11px;color:#374151 !important;background:#fff;cursor:pointer}' +
463      '.' + PREFIX + '-pop-note{font-size:11px;color:#9ca3af !important;padding:6px 0 0}' +
464
465      // Toast
466      '.' + PREFIX + '-toast{position:fixed;bottom:24px;right:24px;padding:12px 20px;border-radius:8px;' +
467        'background:#065f46;color:#fff !important;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;' +
468        'opacity:0;transition:opacity .3s;z-index:99996;pointer-events:none}' +
469      '.' + PREFIX + '-toast.show{opacity:1}' +
470
471      // Mobile
472      '@media(max-width:600px){' +
473        '#' + PREFIX + '-topbar{height:auto;flex-wrap:wrap;padding:6px 8px;gap:6px}' +
474        '.' + PREFIX + '-tb-center{display:none}' +
475        '#' + PREFIX + '-popover{position:fixed;bottom:0;left:0;right:0;top:auto !important;width:100%;' +
476          'border-radius:12px 12px 0 0;max-height:50vh;overflow-y:auto;transform:translateY(100%)}' +
477        '#' + PREFIX + '-popover.open{transform:translateY(0)}' +
478      '}';
479    document.head.appendChild(s);
480  }
481
482  // === CREATE UI ELEMENTS ===
483
484  function createFAB() {
485    fabEl = document.createElement('button');
486    fabEl.id = PREFIX + '-fab';
487    fabEl.setAttribute('aria-label', 'Site Appearance');
488    fabEl.setAttribute('title', 'Site Appearance (Admin)');
489    fabEl.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.53 16.12a3 3 0 0 0-5.78 1.13 2.25 2.25 0 0 1-2.4 2.24 4.5 4.5 0 0 0 8.4-2.24c0-.4-.08-.78-.22-1.13Zm0 0a16 16 0 0 0 3.39-1.62m-5.04-.02a16 16 0 0 1 1.62-3.4m3.42 3.42a16 16 0 0 0 4.76-4.64l3.88-5.82a1.15 1.15 0 0 0-1.6-1.6l-5.81 3.88a16 16 0 0 0-4.65 4.76m3.42 3.42a6.78 6.78 0 0 0-3.42-3.42"/></svg>';
490    fabEl.addEventListener('click', toggleDesignMode);
491    document.documentElement.appendChild(fabEl);
492    _tryMoveToBar();
493  }
494
495  function _insertAfterDragDrop(container) {
496    var ddBtn = container.querySelector('#sys-bl-fab');
497    if (ddBtn && ddBtn.nextSibling) {
498      container.insertBefore(fabEl, ddBtn.nextSibling);
499    } else if (ddBtn) {
500      container.appendChild(fabEl);
501    } else {
502      container.insertBefore(fabEl, container.firstChild);
503    }
504  }
505
506  function _tryMoveToBar() {
507    var lnks = document.querySelector('#cms-lb-hdr .lnks');
508    if (lnks && fabEl && fabEl.parentNode !== lnks) {
509      fabEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:middle;margin-right:4px"><path d="M9.53 16.12a3 3 0 0 0-5.78 1.13 2.25 2.25 0 0 1-2.4 2.24 4.5 4.5 0 0 0 8.4-2.24c0-.4-.08-.78-.22-1.13Zm0 0a16 16 0 0 0 3.39-1.62m-5.04-.02a16 16 0 0 1 1.62-3.4m3.42 3.42a16 16 0 0 0 4.76-4.64l3.88-5.82a1.15 1.15 0 0 0-1.6-1.6l-5.81 3.88a16 16 0 0 0-4.65 4.76m3.42 3.42a6.78 6.78 0 0 0-3.42-3.42"/></svg>Appearance';
510      fabEl.setAttribute('title', 'Site Appearance');
511      _insertAfterDragDrop(lnks);
512      return;
513    }
514    if (!lnks) {
515      var obs = new MutationObserver(function(muts, o) {
516        var bar = document.querySelector('#cms-lb-hdr .lnks');
517        if (bar && fabEl) {
518          fabEl.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:middle;margin-right:4px"><path d="M9.53 16.12a3 3 0 0 0-5.78 1.13 2.25 2.25 0 0 1-2.4 2.24 4.5 4.5 0 0 0 8.4-2.24c0-.4-.08-.78-.22-1.13Zm0 0a16 16 0 0 0 3.39-1.62m-5.04-.02a16 16 0 0 1 1.62-3.4m3.42 3.42a16 16 0 0 0 4.76-4.64l3.88-5.82a1.15 1.15 0 0 0-1.6-1.6l-5.81 3.88a16 16 0 0 0-4.65 4.76m3.42 3.42a6.78 6.78 0 0 0-3.42-3.42"/></svg>Appearance';
519          fabEl.setAttribute('title', 'Site Appearance');
520          _insertAfterDragDrop(bar);
521          o.disconnect();
522        }
523      });
524      obs.observe(document.documentElement, { childList: true, subtree: true });
525      setTimeout(function() { obs.disconnect(); }, 10000);
526    }
527  }
528
529  function createTopBar() {
530    topBarEl = document.createElement('div');
531    topBarEl.id = PREFIX + '-topbar';
532
533    var html = '';
534
535    // Left section
536    html += '<div class="' + PREFIX + '-tb-left">';
537    html += '<i class="fa fa-pen"></i>';
538    html += '<span class="' + PREFIX + '-tb-label">Styling Mode</span>';
539    html += '<span class="' + PREFIX + '-tb-dot"></span>';
540    html += '</div>';
541
542    // Center section - theme, font size, font family
543    html += '<div class="' + PREFIX + '-tb-center">';
544
545    // Theme dropdown
546    html += '<span class="' + PREFIX + '-tb-group-label">Mode</span>';
547    html += '<select class="' + PREFIX + '-tb-select" id="' + PREFIX + '-tb-theme">';
548    var themes = [
549      { key: '', label: 'Auto' },
550      { key: 'light', label: 'Light' },
551      { key: 'dark', label: 'Dark' },
552      { key: 'sepia', label: 'Sepia' },
553      { key: 'highContrast', label: 'Contrast' }
554    ];
555    for (var i = 0; i < themes.length; i++) {
556      var t = themes[i];
557      var tSel = state.theme === t.key ? ' selected' : '';
558      html += '<option value="' + t.key + '"' + tSel + '>' + t.label + '</option>';
559    }
560    html += '</select>';
561
562    // Font size group
563    html += '<div class="' + PREFIX + '-tb-group">';
564    html += '<span class="' + PREFIX + '-tb-group-label">Size</span>';
565    var sizes = [
566      { key: 0.85, label: 'S' },
567      { key: 1, label: 'M' },
568      { key: 1.15, label: 'L' },
569      { key: 1.3, label: 'XL' }
570    ];
571    for (var j = 0; j < sizes.length; j++) {
572      var sz = sizes[j];
573      var fAct = state.fontScale === sz.key ? ' active' : '';
574      html += '<button class="' + PREFIX + '-tb-btn' + fAct + '" data-act="fontScale" data-val="' + sz.key + '">' + sz.label + '</button>';
575    }
576    html += '</div>';
577
578    // Font family dropdown
579    html += '<select class="' + PREFIX + '-tb-select" id="' + PREFIX + '-tb-font">';
580    var fonts = [
581      { key: 'system', label: 'System' }, { key: 'arial', label: 'Arial' },
582      { key: 'georgia', label: 'Georgia' }, { key: 'times', label: 'Times' },
583      { key: 'verdana', label: 'Verdana' }, { key: 'roboto', label: 'Roboto' },
584      { key: 'opensans', label: 'Open Sans' }, { key: 'lato', label: 'Lato' },
585      { key: 'montserrat', label: 'Montserrat' }, { key: 'poppins', label: 'Poppins' },
586      { key: 'raleway', label: 'Raleway' }, { key: 'nunito', label: 'Nunito' },
587      { key: 'merriweather', label: 'Merriweather' }, { key: 'playfair', label: 'Playfair' }
588    ];
589    for (var f = 0; f < fonts.length; f++) {
590      var ff = fonts[f];
591      var sel = state.fontFamily === ff.key ? ' selected' : '';
592      html += '<option value="' + ff.key + '"' + sel + '>' + ff.label + '</option>';
593    }
594    html += '</select>';
595
596    html += '</div>';
597
598    // Right section
599    html += '<div class="' + PREFIX + '-tb-right">';
600    html += '<button class="' + PREFIX + '-tb-save" id="' + PREFIX + '-tb-save">Save</button>';
601    html += '<button class="' + PREFIX + '-tb-reset" id="' + PREFIX + '-tb-reset">Reset</button>';
602    html += '<button class="' + PREFIX + '-tb-exit" id="' + PREFIX + '-tb-exit" title="Exit Styling Mode">&times;</button>';
603    html += '</div>';
604
605    topBarEl.innerHTML = html;
606    document.documentElement.appendChild(topBarEl);
607
608    // Event: font size buttons
609    topBarEl.addEventListener('click', function(e) {
610      var btn = e.target.closest('[data-act]');
611      if (!btn) return;
612      var act = btn.getAttribute('data-act');
613      var val = btn.getAttribute('data-val');
614
615      if (act === 'fontScale') {
616        state.fontScale = parseFloat(val);
617        applyFontScale(state.fontScale);
618      }
619
620      var btns = topBarEl.querySelectorAll('[data-act="' + act + '"]');
621      for (var i = 0; i < btns.length; i++) {
622        btns[i].classList.toggle('active', btns[i].getAttribute('data-val') === val);
623      }
624    });
625
626    // Event: theme dropdown
627    topBarEl.querySelector('#' + PREFIX + '-tb-theme').addEventListener('change', function() {
628      state.theme = this.value;
629      applyTheme(this.value);
630    });
631
632    // Event: font family dropdown
633    topBarEl.querySelector('#' + PREFIX + '-tb-font').addEventListener('change', function() {
634      state.fontFamily = this.value;
635      applyFontFamily(this.value);
636    });
637
638    // Event: Save
639    topBarEl.querySelector('#' + PREFIX + '-tb-save').addEventListener('click', doSave);
640
641    // Event: Reset
642    topBarEl.querySelector('#' + PREFIX + '-tb-reset').addEventListener('click', doReset);
643
644    // Event: Exit
645    topBarEl.querySelector('#' + PREFIX + '-tb-exit').addEventListener('click', exitDesignMode);
646  }
647
648  function createHighlight() {
649    highlightEl = document.createElement('div');
650    highlightEl.id = PREFIX + '-highlight';
651    highlightEl.innerHTML = '<span class="' + PREFIX + '-cat-badge"></span>';
652    document.documentElement.appendChild(highlightEl);
653  }
654
655  function createPopover() {
656    popoverEl = document.createElement('div');
657    popoverEl.id = PREFIX + '-popover';
658    document.documentElement.appendChild(popoverEl);
659
660    // Event delegation for color inputs inside popover
661    popoverEl.addEventListener('input', function(e) {
662      if (e.target.type !== 'color') return;
663      var key = e.target.getAttribute('data-color');
664      var menuKey = e.target.getAttribute('data-menu-key');
665      var menuProp = e.target.getAttribute('data-menu-prop');
666      if (menuKey && menuProp) {
667        // Per-menu-item color
668        if (!state.menuStyles[menuKey]) state.menuStyles[menuKey] = {};
669        state.menuStyles[menuKey][menuProp] = e.target.value;
670        var hex = popoverEl.querySelector('.hex[data-for="' + menuKey + '-' + menuProp + '"]');
671        if (hex) hex.textContent = e.target.value;
672        applyMenuStyles();
673      } else if (key) {
674        // Category color
675        state.colors[key] = e.target.value;
676        var hexEl = popoverEl.querySelector('.hex[data-for="' + key + '"]');
677        if (hexEl) hexEl.textContent = e.target.value;
678        applyColors();
679      }
680    });
681
682    // Style option buttons (fontWeight, textTransform)
683    popoverEl.addEventListener('click', function(e) {
684      // Close button
685      if (e.target.closest('.' + PREFIX + '-pop-close')) {
686        hidePopover();
687        return;
688      }
689      var opt = e.target.closest('.' + PREFIX + '-pop-opt');
690      if (!opt) return;
691      var prop = opt.getAttribute('data-style-prop');
692      var val = opt.getAttribute('data-style-val');
693      var catKey = opt.getAttribute('data-style-cat');
694      var menuKey = opt.getAttribute('data-style-menu');
695
696      if (menuKey) {
697        if (!state.menuStyles[menuKey]) state.menuStyles[menuKey] = {};
698        state.menuStyles[menuKey][prop] = val || null;
699        applyMenuStyles();
700      } else if (catKey) {
701        if (!state.categoryStyles[catKey]) state.categoryStyles[catKey] = {};
702        state.categoryStyles[catKey][prop] = val || null;
703        applyCategoryStyles();
704      }
705      // Update active state for sibling buttons
706      var siblings = opt.parentNode.querySelectorAll('.' + PREFIX + '-pop-opt');
707      for (var i = 0; i < siblings.length; i++) siblings[i].classList.remove('active');
708      opt.classList.add('active');
709    });
710
711    // Style select dropdowns (fontSize)
712    popoverEl.addEventListener('change', function(e) {
713      var sel = e.target;
714      if (!sel.hasAttribute('data-style-prop')) return;
715      var prop = sel.getAttribute('data-style-prop');
716      var val = sel.value;
717      var catKey = sel.getAttribute('data-style-cat');
718      var menuKey = sel.getAttribute('data-style-menu');
719
720      if (menuKey) {
721        if (!state.menuStyles[menuKey]) state.menuStyles[menuKey] = {};
722        state.menuStyles[menuKey][prop] = val || null;
723        applyMenuStyles();
724      } else if (catKey) {
725        if (!state.categoryStyles[catKey]) state.categoryStyles[catKey] = {};
726        state.categoryStyles[catKey][prop] = val || null;
727        applyCategoryStyles();
728      }
729    });
730  }
731
732  function createToast() {
733    toastEl = document.createElement('div');
734    toastEl.className = PREFIX + '-toast';
735    toastEl.id = PREFIX + '-toast';
736    document.documentElement.appendChild(toastEl);
737  }
738
739  // === DESIGN MODE ===
740
741  function toggleDesignMode() {
742    if (designMode) exitDesignMode(); else enterDesignMode();
743  }
744
745  function enterDesignMode() {
746    designMode = true;
747    topBarEl.classList.add('open');
748
749    // Push page content down
750    var barOffset = _getBarOffset();
751    document.body.style.paddingTop = (barOffset + 56) + 'px';
752    document.body.style.transition = 'padding-top 0.3s ease';
753
754    // Attach event listeners (capture phase)
755    document.addEventListener('mouseover', onDesignHover, true);
756    document.addEventListener('mouseout', onDesignHoverOut, true);
757    document.addEventListener('click', onDesignClick, true);
758    document.addEventListener('keydown', onDesignKeydown);
759    window.addEventListener('scroll', onScrollOrResize, { passive: true });
760    window.addEventListener('resize', onScrollOrResize, { passive: true });
761
762    // Hide FAB
763    fabEl.style.display = 'none';
764
765    // Hide all Live Editor elements while Styling Mode is active
766    if (!document.getElementById(PREFIX + '-hide-lb')) {
767      var lbHide = document.createElement('style');
768      lbHide.id = PREFIX + '-hide-lb';
769      lbHide.textContent = '#cms-lb-hdr,[class^="cms-lb-ov"],[class*=" cms-lb-ov"],[class^="cms-lb-mi"],[class*=" cms-lb-mi"],[class^="cms-lb-ad"],[class*=" cms-lb-ad"],[class^="cms-lb-el"],[class*=" cms-lb-el"]{display:none !important}';
770      document.head.appendChild(lbHide);
771    }
772  }
773
774  function exitDesignMode() {
775    designMode = false;
776    topBarEl.classList.remove('open');
777    document.body.style.paddingTop = '';
778    document.body.style.transition = '';
779
780    document.removeEventListener('mouseover', onDesignHover, true);
781    document.removeEventListener('mouseout', onDesignHoverOut, true);
782    document.removeEventListener('click', onDesignClick, true);
783    document.removeEventListener('keydown', onDesignKeydown);
784    window.removeEventListener('scroll', onScrollOrResize);
785    window.removeEventListener('resize', onScrollOrResize);
786
787    hideHighlight();
788    hidePopover();
789
790    fabEl.style.display = '';
791
792    // Restore Live Editor elements
793    var lbHide = document.getElementById(PREFIX + '-hide-lb');
794    if (lbHide) lbHide.remove();
795  }
796
797  function _getBarOffset() {
798    var liveBar = document.getElementById('cms-lb-hdr');
799    if (liveBar) return liveBar.offsetHeight || 0;
800    return 0;
801  }
802
803  // === HOVER HIGHLIGHT ===
804
805  var _hoverTick = false;
806
807  function onDesignHover(e) {
808    var el = e.target;
809    if (el.closest('#' + PREFIX + '-topbar,#' + PREFIX + '-popover,#' + PREFIX + '-highlight,#' + PREFIX + '-fab,#sys-up-fab,#sys-up-panel')) return;
810
811    if (_hoverTick) return;
812    _hoverTick = true;
813    requestAnimationFrame(function() {
814      _hoverTick = false;
815      var cat = detectCategory(el);
816      if (!cat) { hideHighlight(); return; }
817      showHighlight(el, cat);
818    });
819  }
820
821  function onDesignHoverOut(e) {
822    if (e.relatedTarget && (
823      (e.relatedTarget.closest && e.relatedTarget.closest('#' + PREFIX + '-highlight')) ||
824      e.relatedTarget === highlightEl
825    )) return;
826    // Don't hide if moving within the same element
827    if (e.relatedTarget && _highlightTarget && _highlightTarget.contains(e.relatedTarget)) return;
828    hideHighlight();
829  }
830
831  function showHighlight(el, cat) {
832    // For menu/submenu items, get the actual container element
833    var targetEl = el;
834    if (cat.key === 'menuItem' || cat.key === 'submenuItem') {
835      var mi = detectMenuItem(el);
836      if (mi) targetEl = mi.el;
837    }
838
839    _highlightTarget = targetEl;
840    var rect = targetEl.getBoundingClientRect();
841    var pad = 2;
842
843    highlightEl.style.top = (rect.top - pad) + 'px';
844    highlightEl.style.left = (rect.left - pad) + 'px';
845    highlightEl.style.width = (rect.width + pad * 2) + 'px';
846    highlightEl.style.height = (rect.height + pad * 2) + 'px';
847    highlightEl.style.borderColor = cat.color;
848
849    var badge = highlightEl.querySelector('.' + PREFIX + '-cat-badge');
850    var label = cat.label;
851    if (cat.key === 'menuItem' || cat.key === 'submenuItem') {
852      var mi2 = detectMenuItem(el);
853      if (mi2) label = (mi2.type === 'menu' ? 'Menu: ' : 'Submenu: ') + mi2.label;
854    }
855    badge.textContent = label;
856    badge.style.background = cat.color;
857
858    highlightEl.classList.add('visible');
859  }
860
861  function hideHighlight() {
862    highlightEl.classList.remove('visible');
863    _highlightTarget = null;
864  }
865
866  function repositionHighlight() {
867    if (!_highlightTarget) return;
868    var rect = _highlightTarget.getBoundingClientRect();
869    var pad = 2;
870    highlightEl.style.top = (rect.top - pad) + 'px';
871    highlightEl.style.left = (rect.left - pad) + 'px';
872    highlightEl.style.width = (rect.width + pad * 2) + 'px';
873    highlightEl.style.height = (rect.height + pad * 2) + 'px';
874  }
875
876  // === CLICK POPOVER ===
877
878  function onDesignClick(e) {
879    var el = e.target;
880    // Allow clicks on our own UI
881    if (el.closest('#' + PREFIX + '-topbar,#' + PREFIX + '-popover,#sys-up-fab,#sys-up-panel')) return;
882
883    e.preventDefault();
884    e.stopPropagation();
885
886    var cat = detectCategory(el);
887    if (!cat) { hidePopover(); return; }
888
889    // For menu/submenu items, show per-item popover
890    if (cat.key === 'menuItem' || cat.key === 'submenuItem') {
891      var mi = detectMenuItem(el);
892      if (mi && mi.id) {
893        showMenuPopover(mi, cat);
894        return;
895      }
896    }
897
898    showCategoryPopover(el, cat);
899  }
900
901  function showCategoryPopover(el, cat) {
902    _popoverTarget = el;
903    _popoverCategory = cat;
904    _popoverMenuItem = null;
905
906    // Only disable color pickers when admin explicitly chose a non-light theme (not Auto)
907    var isExplicitNonLight = state.theme === 'dark' || state.theme === 'sepia' || state.theme === 'highContrast';
908
909    var html = '';
910    html += '<div class="' + PREFIX + '-pop-hdr">';
911    html += '<div class="' + PREFIX + '-pop-title"><span class="' + PREFIX + '-pop-dot" style="background:' + cat.color + '"></span>' + cat.label + '</div>';
912    html += '<button class="' + PREFIX + '-pop-close">&times;</button>';
913    html += '</div>';
914    html += '<div class="' + PREFIX + '-pop-body">';
915
916    if (isExplicitNonLight && cat.controls.length > 0) {
917      html += '<div class="' + PREFIX + '-pop-note">Color pickers disabled in ' + state.theme + ' theme. Switch to Light or Auto to edit colors.</div>';
918    } else {
919      for (var i = 0; i < cat.controls.length; i++) {
920        var key = cat.controls[i];
921        var val = state.colors[key] || DEFAULTS[key];
922        var lbl = COLOR_LABELS[key] || key;
923        html += '<div class="' + PREFIX + '-pop-row">';
924        html += '<label>' + lbl + '</label>';
925        html += '<input type="color" data-color="' + key + '" value="' + val + '">';
926        html += '<span class="hex" data-for="' + key + '">' + val + '</span>';
927        html += '</div>';
928      }
929    }
930
931    if (cat.controls.length === 0 && cat.key !== 'menuItem' && cat.key !== 'submenuItem' && !STYLE_CONTROLS[cat.key]) {
932      html += '<div class="' + PREFIX + '-pop-note">Click a specific element for styling options.</div>';
933    }
934
935    // Style controls (fontWeight, fontSize, textTransform)
936    var styleKeys = STYLE_CONTROLS[cat.key];
937    if (styleKeys) {
938      var existing = state.categoryStyles[cat.key] || {};
939      html += _buildStyleControlsHtml(styleKeys, existing, 'data-style-cat="' + cat.key + '"');
940    }
941
942    html += '</div>';
943    popoverEl.innerHTML = html;
944    popoverEl.classList.add('open');
945    repositionPopover(el);
946  }
947
948  function showMenuPopover(mi, cat) {
949    _popoverTarget = mi.el;
950    _popoverCategory = cat;
951    _popoverMenuItem = mi;
952
953    var menuKey = mi.type + '_' + mi.id;
954    var existing = state.menuStyles[menuKey] || {};
955
956    var html = '';
957    html += '<div class="' + PREFIX + '-pop-hdr">';
958    html += '<div class="' + PREFIX + '-pop-title"><span class="' + PREFIX + '-pop-dot" style="background:' + cat.color + '"></span>';
959    html += (mi.type === 'menu' ? 'Menu' : 'Submenu') + ': ' + _escHtml(mi.label) + '</div>';
960    html += '<button class="' + PREFIX + '-pop-close">&times;</button>';
961    html += '</div>';
962    html += '<div class="' + PREFIX + '-pop-body">';
963
964    // BG color
965    var bgVal = existing.bg || (mi.type === 'menu' ? (state.colors.navBg || DEFAULTS.navBg) : (state.colors.dropdownBg || DEFAULTS.dropdownBg));
966    html += '<div class="' + PREFIX + '-pop-row">';
967    html += '<label>BG Color</label>';
968    html += '<input type="color" data-menu-key="' + menuKey + '" data-menu-prop="bg" value="' + bgVal + '">';
969    html += '<span class="hex" data-for="' + menuKey + '-bg">' + bgVal + '</span>';
970    html += '</div>';
971
972    // Text color
973    var textVal = existing.text || (mi.type === 'menu' ? (state.colors.navText || DEFAULTS.navText) : (state.colors.dropdownText || DEFAULTS.dropdownText));
974    html += '<div class="' + PREFIX + '-pop-row">';
975    html += '<label>Text Color</label>';
976    html += '<input type="color" data-menu-key="' + menuKey + '" data-menu-prop="text" value="' + textVal + '">';
977    html += '<span class="hex" data-for="' + menuKey + '-text">' + textVal + '</span>';
978    html += '</div>';
979
980    // Style controls for menu items
981    var menuStyleKeys = STYLE_CONTROLS[cat.key] || [];
982    if (menuStyleKeys.length) {
983      html += _buildStyleControlsHtml(menuStyleKeys, existing, 'data-style-menu="' + menuKey + '"');
984    }
985
986    // no note needed
987    html += '</div>';
988
989    popoverEl.innerHTML = html;
990    popoverEl.classList.add('open');
991    repositionPopover(mi.el);
992  }
993
994  function hidePopover() {
995    popoverEl.classList.remove('open');
996    _popoverTarget = null;
997    _popoverCategory = null;
998    _popoverMenuItem = null;
999  }
1000
1001  function repositionPopover(targetEl) {
1002    if (!targetEl) return;
1003    var rect = targetEl.getBoundingClientRect();
1004    var popW = 280;
1005    var popH = popoverEl.offsetHeight || 160;
1006    var vpW = window.innerWidth;
1007    var vpH = window.innerHeight;
1008    var margin = 8;
1009    var topBarH = 56 + _getBarOffset();
1010
1011    // Mobile: use bottom sheet positioning (CSS handles it)
1012    if (vpW <= 600) {
1013      popoverEl.style.top = '';
1014      popoverEl.style.left = '';
1015      return;
1016    }
1017
1018    // Preferred: below the element
1019    var top = rect.bottom + margin;
1020    var left = rect.left + (rect.width / 2) - (popW / 2);
1021
1022    // If below overflows, place above
1023    if (top + popH > vpH - margin) {
1024      top = rect.top - popH - margin;
1025    }
1026    // If above overflows (too close to top bar), place beside
1027    if (top < topBarH + margin) {
1028      top = Math.max(topBarH + margin, rect.top);
1029      left = rect.right + margin;
1030      if (left + popW > vpW - margin) {
1031        left = rect.left - popW - margin;
1032      }
1033    }
1034
1035    left = Math.max(margin, Math.min(left, vpW - popW - margin));
1036    top = Math.max(topBarH + margin, top);
1037
1038    popoverEl.style.top = top + 'px';
1039    popoverEl.style.left = left + 'px';
1040  }
1041
1042  // === SCROLL/RESIZE ===
1043
1044  function onScrollOrResize() {
1045    if (_rafId) return;
1046    _rafId = requestAnimationFrame(function() {
1047      _rafId = null;
1048      if (_highlightTarget) repositionHighlight();
1049      if (_popoverTarget) {
1050        var rect = _popoverTarget.getBoundingClientRect();
1051        var vpH = window.innerHeight;
1052        // Hide popover if target scrolled out of view
1053        if (rect.bottom < 56 || rect.top > vpH) {
1054          hidePopover();
1055        } else {
1056          repositionPopover(_popoverTarget);
1057        }
1058      }
1059    });
1060  }
1061
1062  // === KEYBOARD ===
1063
1064  function onDesignKeydown(e) {
1065    if (e.key === 'Escape') {
1066      if (popoverEl.classList.contains('open')) {
1067        hidePopover();
1068      } else {
1069        exitDesignMode();
1070      }
1071    }
1072  }
1073
1074  // === SAVE / RESET ===
1075
1076  function doSave() {
1077    var btn = topBarEl.querySelector('#' + PREFIX + '-tb-save');
1078    btn.disabled = true;
1079    btn.textContent = 'Saving...';
1080
1081    var c = state.colors;
1082    var colorsObj = null;
1083    var setKeys = dk.filter(function(k) { return !!c[k]; });
1084    if (setKeys.length) {
1085      colorsObj = {};
1086      setKeys.forEach(function(k) { colorsObj[k] = c[k]; });
1087    }
1088
1089    var menuStylesObj = null;
1090    var msKeys = Object.keys(state.menuStyles);
1091    if (msKeys.length) {
1092      menuStylesObj = {};
1093      msKeys.forEach(function(k) { menuStylesObj[k] = state.menuStyles[k]; });
1094    }
1095
1096    var categoryStylesObj = null;
1097    var csKeys = Object.keys(state.categoryStyles);
1098    if (csKeys.length) {
1099      categoryStylesObj = {};
1100      csKeys.forEach(function(k) { categoryStylesObj[k] = state.categoryStyles[k]; });
1101    }
1102
1103    var body = {
1104      appearance: {
1105        theme: state.theme || null,
1106        fontScale: state.fontScale,
1107        spacingScale: state.spacingScale,
1108        fontFamily: state.fontFamily || 'system',
1109        colors: colorsObj,
1110        menuStyles: menuStylesObj,
1111        categoryStyles: categoryStylesObj
1112      }
1113    };
1114
1115    fetch(_bp + '/api/sys-settings', {
1116      method: 'PUT',
1117      headers: { 'Content-Type': 'application/json' },
1118      body: JSON.stringify(body)
1119    })
1120    .then(function(r) { return r.json(); })
1121    .then(function(j) {
1122      if (j.success) {
1123        toast('Saved!', true);
1124        try { sessionStorage.setItem(PREFIX + '-refresh', String(Date.now())); } catch(e) {}
1125        setTimeout(function() { _hotReloadAssets(); }, 3000);
1126      } else {
1127        toast('Error: ' + (j.error || 'Save failed'), false);
1128      }
1129      btn.disabled = false;
1130      btn.textContent = 'Save';
1131    })
1132    .catch(function(e) {
1133      toast('Error: ' + e.message, false);
1134      btn.disabled = false;
1135      btn.textContent = 'Save';
1136    });
1137  }
1138
1139  function doReset() {
1140    var resetBtn = topBarEl.querySelector('#' + PREFIX + '-tb-reset');
1141    resetBtn.textContent = 'Resetting...';
1142
1143    // Reset state
1144    state.theme = '';
1145    state.fontScale = 1;
1146    state.spacingScale = 1;
1147    state.fontFamily = 'system';
1148    state.colors = {};
1149    state.menuStyles = {};
1150    state.categoryStyles = {};
1151    dk.forEach(function(k) { state.colors[k] = DEFAULTS[k]; });
1152
1153    // Update top bar active states
1154    var allBtns = topBarEl.querySelectorAll('[data-act]');
1155    for (var i = 0; i < allBtns.length; i++) {
1156      var b = allBtns[i];
1157      var act = b.getAttribute('data-act');
1158      var val = b.getAttribute('data-val');
1159      if (act === 'theme') b.classList.toggle('active', val === '');
1160      else if (act === 'fontScale') b.classList.toggle('active', val === '1');
1161    }
1162
1163    // Reset theme dropdown
1164    var themeSel = topBarEl.querySelector('#' + PREFIX + '-tb-theme');
1165    if (themeSel) themeSel.value = '';
1166
1167    // Reset font dropdown
1168    var fontSel = topBarEl.querySelector('#' + PREFIX + '-tb-font');
1169    if (fontSel) fontSel.value = 'system';
1170
1171    // Apply visual reset
1172    document.documentElement.setAttribute('data-theme', 'light');
1173    document.body.style.filter = '';
1174    document.body.style.background = '';
1175    var overrideEl = document.getElementById(PREFIX + '-overrides');
1176    if (overrideEl) overrideEl.textContent = '';
1177
1178    applyFontScale(1);
1179    applySpacingScale(1);
1180    applyFontFamily('system');
1181
1182    // Force-apply default colors using same high-specificity selectors
1183    var colorEl = document.getElementById(PREFIX + '-colors');
1184    if (!colorEl) { colorEl = document.createElement('style'); colorEl.id = PREFIX + '-colors'; document.head.appendChild(colorEl); }
1185    colorEl.textContent = _buildColorCss(state.colors);
1186    document.head.appendChild(colorEl);
1187
1188    // Clear menu styles and category styles
1189    var menuEl = document.getElementById(PREFIX + '-menu-colors');
1190    if (menuEl) menuEl.textContent = '';
1191    var catStyleEl = document.getElementById(PREFIX + '-cat-styles');
1192    if (catStyleEl) catStyleEl.textContent = '';
1193
1194    // Clear visitor widget colors
1195    var upColors = document.getElementById('sys-up-admin-colors');
1196    if (upColors) upColors.textContent = '';
1197
1198    // Hide popover since everything is reset
1199    hidePopover();
1200
1201    // Save defaults to server
1202    var saveBtn = topBarEl.querySelector('#' + PREFIX + '-tb-save');
1203    saveBtn.disabled = true;
1204    saveBtn.textContent = 'Saving...';
1205
1206    var body = {
1207      appearance: null,
1208      _resetAppearance: true
1209    };
1210
1211    fetch(_bp + '/api/sys-settings', {
1212      method: 'PUT',
1213      headers: { 'Content-Type': 'application/json' },
1214      body: JSON.stringify(body)
1215    })
1216    .then(function(r) { return r.json(); })
1217    .then(function(j) {
1218      if (j.success) {
1219        toast('Reset & saved! Reloading…', true);
1220        // Show spinner overlay while waiting for regeneration + reload
1221        _showResetOverlay();
1222        // Allow background CSS/page regeneration to finish, then hard-reload
1223        setTimeout(function() { location.reload(true); }, 3500);
1224      } else {
1225        toast('Error: ' + (j.error || 'Reset failed'), false);
1226        saveBtn.disabled = false;
1227        saveBtn.textContent = 'Save';
1228        resetBtn.textContent = 'Reset';
1229      }
1230    })
1231    .catch(function(e) {
1232      toast('Error: ' + e.message, false);
1233      saveBtn.disabled = false;
1234      saveBtn.textContent = 'Save';
1235      resetBtn.textContent = 'Reset';
1236    });
1237  }
1238
1239  // === UTILITIES ===
1240
1241  function toast(msg, ok) {
1242    var t = document.getElementById(PREFIX + '-toast');
1243    if (!t) return;
1244    t.textContent = msg;
1245    t.style.background = ok ? '#065f46' : '#991b1b';
1246    t.classList.add('show');
1247    setTimeout(function() { t.classList.remove('show'); }, 3000);
1248  }
1249
1250  function _buildStyleControlsHtml(controlKeys, existingStyles, dataAttr) {
1251    // dataAttr is e.g. 'data-style-cat="heading"' or 'data-style-menu="menu_5"'
1252    var html = '';
1253    for (var i = 0; i < controlKeys.length; i++) {
1254      var prop = controlKeys[i];
1255      var curVal = (existingStyles && existingStyles[prop]) || '';
1256
1257      html += '<div class="' + PREFIX + '-pop-row">';
1258      html += '<label>' + (STYLE_LABELS[prop] || prop) + '</label>';
1259
1260      if (prop === 'fontWeight') {
1261        html += '<div class="' + PREFIX + '-pop-opts">';
1262        html += '<button class="' + PREFIX + '-pop-opt' + (curVal !== 'bold' ? ' active' : '') + '" data-style-prop="fontWeight" data-style-val="" ' + dataAttr + '>Normal</button>';
1263        html += '<button class="' + PREFIX + '-pop-opt' + (curVal === 'bold' ? ' active' : '') + '" data-style-prop="fontWeight" data-style-val="bold" ' + dataAttr + '>Bold</button>';
1264        html += '</div>';
1265      } else if (prop === 'textTransform') {
1266        html += '<div class="' + PREFIX + '-pop-opts">';
1267        html += '<button class="' + PREFIX + '-pop-opt' + (!curVal ? ' active' : '') + '" data-style-prop="textTransform" data-style-val="" ' + dataAttr + '>Aa</button>';
1268        html += '<button class="' + PREFIX + '-pop-opt' + (curVal === 'uppercase' ? ' active' : '') + '" data-style-prop="textTransform" data-style-val="uppercase" ' + dataAttr + '>AA</button>';
1269        html += '<button class="' + PREFIX + '-pop-opt' + (curVal === 'capitalize' ? ' active' : '') + '" data-style-prop="textTransform" data-style-val="capitalize" ' + dataAttr + '>Ab</button>';
1270        html += '</div>';
1271      } else if (prop === 'fontSize') {
1272        html += '<select class="' + PREFIX + '-pop-sel" data-style-prop="fontSize" ' + dataAttr + '>';
1273        var sizes = [
1274          { v: '', l: 'Default' }, { v: '0.85', l: 'Small' }, { v: '1', l: 'Normal' },
1275          { v: '1.15', l: 'Large' }, { v: '1.3', l: 'X-Large' }, { v: '1.5', l: 'XX-Large' }
1276        ];
1277        for (var s = 0; s < sizes.length; s++) {
1278          html += '<option value="' + sizes[s].v + '"' + (curVal === sizes[s].v ? ' selected' : '') + '>' + sizes[s].l + '</option>';
1279        }
1280        html += '</select>';
1281      } else if (prop === 'borderRadius') {
1282        html += '<div class="' + PREFIX + '-pop-opts">';
1283        html += '<button class="' + PREFIX + '-pop-opt' + (!curVal ? ' active' : '') + '" data-style-prop="borderRadius" data-style-val="" ' + dataAttr + '>Default</button>';
1284        html += '<button class="' + PREFIX + '-pop-opt' + (curVal === '0' ? ' active' : '') + '" data-style-prop="borderRadius" data-style-val="0" ' + dataAttr + '>Sharp</button>';
1285        html += '<button class="' + PREFIX + '-pop-opt' + (curVal === '20' ? ' active' : '') + '" data-style-prop="borderRadius" data-style-val="20" ' + dataAttr + '>Round</button>';
1286        html += '</div>';
1287      }
1288
1289      html += '</div>';
1290    }
1291    return html;
1292  }
1293
1294  function _escHtml(str) {
1295    var d = document.createElement('div');
1296    d.textContent = str;
1297    return d.innerHTML;
1298  }
1299
1300  function _showResetOverlay() {
1301    var ov = document.createElement('div');
1302    ov.id = PREFIX + '-reset-overlay';
1303    ov.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(255,255,255,0.75);z-index:999999;display:flex;align-items:center;justify-content:center;flex-direction:column';
1304    ov.innerHTML = '<div style="width:48px;height:48px;border:4px solid #e5e7eb;border-top-color:#ea580c;border-radius:50%;animation:' + PREFIX + '-spin 0.8s linear infinite"></div>' +
1305      '<div style="margin-top:16px;font:600 15px/1 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;color:#374151">Resetting styles…</div>';
1306    // inject spin keyframes if not already present
1307    if (!document.getElementById(PREFIX + '-spin-css')) {
1308      var sk = document.createElement('style');
1309      sk.id = PREFIX + '-spin-css';
1310      sk.textContent = '@keyframes ' + PREFIX + '-spin{to{transform:rotate(360deg)}}';
1311      document.head.appendChild(sk);
1312    }
1313    document.body.appendChild(ov);
1314  }
1315
1316  function _hotReloadAssets() {
1317    var cb = '_v=' + Date.now();
1318    var links = document.querySelectorAll('link[href*="custom.css"]');
1319    for (var i = 0; i < links.length; i++) {
1320      var href = links[i].href.split('?')[0];
1321      links[i].href = href + '?' + cb;
1322    }
1323    var sc = document.createElement('script');
1324    sc.src = '/settings.js?' + cb;
1325    document.head.appendChild(sc);
1326  }
1327
1328  // === INIT ===
1329
1330  function init() {
1331    try {
1332      var ts = sessionStorage.getItem(PREFIX + '-refresh');
1333      if (ts && (Date.now() - parseInt(ts, 10)) < 60000) {
1334        sessionStorage.removeItem(PREFIX + '-refresh');
1335        _hotReloadAssets();
1336      }
1337    } catch(e) {}
1338
1339    function buildUI() {
1340      injectStyles();
1341      createFAB();
1342      createTopBar();
1343      createHighlight();
1344      createPopover();
1345      createToast();
1346      // Apply any saved menu/category styles on load
1347      applyMenuStyles();
1348      applyCategoryStyles();
1349    }
1350    if (document.readyState === 'loading') {
1351      document.addEventListener('DOMContentLoaded', buildUI);
1352    } else {
1353      buildUI();
1354    }
1355  }
1356
1357  init();
1358})();

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.